「手把手系列1」React实战demo:星巴克菜单组件/移动端适配/Tab切换
一、前言
本项目是为了给
react新手入门做的一个仿星巴克菜单组件demo,如果能帮助到您,还请给一个小小的点赞
1.1、项目展示

1.1、可以学到的小知识
- 组件之间的传值(父传子,子传父)
- 切换
navbar进行数据的筛选 - 移动端的不同机型
适配(rem) - SPA单页应用
Footer的实现(还未实现除菜单以外的组件)
二、项目目录搭建
starbucks-demo
-> api
->request.js(处理接口数据)
-> assets
->reset.css(样式重置)
-> components
->emptylist
->footer
->listitem
->menu(实现路由跳转以后应该放在pages目录下)
->navbar
->public/js(实现移动端样式适配)
三、项目实现流程
3.0、技术涉及
WEUI中的Toast组件,数据未加载时,展示loading圈圈axios配合fastmock,模拟从接口拿到数据的过程styled-components:实现css in js,还可以实现css的嵌套,对于写样式十分方便classnames:避免多类名因为引号而不起作用
3.1、切分页面组成及数据准备
1. 切分页面

- 这样一个
menu组件,利用组件化的思维,我们可以切分成TabBar,GoodItem,Footer这样三个组件 - 所以我们的页面组成应该如下
<Wrapper> {/* 1.NavBar组件 */} <NavBar tab={tab} Fn={Fn}/> {/* 2.过渡动画Toast,没有数据的时候展示,引入了WEUI框架 */} {<Toast show={loading} icon="loading">加载中...</Toast>} {/* 2.ListItem商品展示组件*/} {menuList.length>0 ? <ListItem menuList={menuList}/> : <EmptyItem/>} {/* 3.frap搜索按钮,没有封装成组件因为他固定在这里,而且没有复用的机会*/} <div className="frap"> <button id="featured-campaign-search" className="button_primary" rel="menu-search-overlay">搜索菜单</button> </div> </Wrapper>
2. 数据准备
- 你可以尝试访问这里拿到接口的数据:www.fastmock.site/mock/5321bf…
3.2、第一个组件:NavBar
1. tab切换与样式改变
-
这个组件重点在于控制
tabs的切换给li标签加上actice样式,在点击事件的同时,通过执行父组件传过来的Fn函数,同步的改变父组件当中的tab属性(实现了子组件传父组件) -
这里有一个小
重点,为什么onClick函数需要用箭头函数?这是因为箭头函数不会绑定自己的this,如果不用箭头函数,我们需要用bind手动绑定函数的this,不然onClick不会指向当前组件对象,如果你还需要传参数或者控制事件(event)用箭头函数更佳。 -
最后通过
tab状态的改变与active类名相对应,实现MVVM//父组件Menu const [tab,setTab] = useState("全部") const Fn = (tab) =>{ setTab(tab) } <NavBar tab={tab} Fn={Fn}/>//这里给NavBar传入了tab属性和Fn函数//子组件NavBar export default function NavBar(props) { const {tab,Fn} = props//从父组件Menu中解构出tab数据,Fn函数 const changeTab = (tabname)=>{ Fn && Fn(tabname);//执行点击事件的同时通过Fn函数将tab数据传回给Menu组件 } return ( <Wrapper> <nav className='nav-title'>菜单</nav> <div className="tabs-wrapper"> <ul className='subcategories'> {/* onClick绑定li的tab状态修改,并通过Fn函数传至Menu组件实现MVVM */} <li className={tab=="全部"?'active':""} onClick={()=>changeTab("全部")}>全部</li> <li className={tab=="饮料"?'active':""} onClick={()=>changeTab("饮料")}>饮料</li> <li className={tab=="美食"?'active':""} onClick={()=>changeTab("美食")}>美食</li> <li className={tab=="咖啡产品"?'active':""} onClick={()=>changeTab("咖啡产品")}>咖啡产品</li> <li className={tab=="商品"?'active':""} onClick={()=>changeTab("商品")}>商品</li> </ul> </div> </Wrapper> )//css li{ display: inline-block; padding-top: 0.6rem; padding-bottom: 0.15rem; margin-right: 0.9rem; //&:父亲选择器,等同于li.active{} &.active{ //点击下方小绿条的实现 border-bottom: 0.15rem solid rgb(0, 168, 98); color: rgba(0, 0, 0, 0.87); font-weight: 700; transition: all 0.2s; }
2. 实现数据的过滤
- 本来应该放在
menu组件讲的,但是为了文章节奏,我们在这里讲清楚数据的过滤。 - 在文章
3.0时,我们从fastmock接口网站拿到了自定义的数据,接下来我们需要在拿到数据的同时实现数据过滤。 - 后端通过
axios.get得到的数据的同时通过menu组件传过来的tab属性值(因为现在只讲了NavBar组件,你也可以理解为NavBar中的tab属性,这两个在属性上是一样的)对数据数组进行filter操作 - 要实现切换改变商品数据还有一步必不可少,要用
useEffect()去监听tab属性的改变,改变了就重新加载一次,后续menu组件我们还会讲到import axios from 'axios' export const getMenuList = ({tab}) => axios.get('https://www.fastmock.site/mock/5321bf649d06645c4266f3e0d45ae1cc/menu/all') .then ( list => { let remainlist=list.data; if(tab){ switch(tab) { case "全部": remainlist=remainlist; break; case "饮料": remainlist=remainlist.filter(item => item.status==1); break; case "美食": remainlist=remainlist.filter(item => item.status==2); break; case "咖啡产品": remainlist=remainlist.filter(item => item.status==3); break; case "商品": remainlist=remainlist.filter(item => item.status==4); break; default: break; } } return Promise.resolve({ remainlist }); } )
3.3、第二个组件:ListItem

- 这个组件的功能主要是实现
接口数据的展示,并封装样式输出 - 我们可以在遍历的时候以组件
Good的形式输出,然后在Good组件中我们可以更好的封装样式(主要用到弹性布局)import React from 'react' import { Wrapper,GoodWrapper } from './style' const Good = ({goodItem}) => ( <GoodWrapper> <div className="good"> <img src={goodItem.img} alt=""/> <div className="name">{goodItem.goods}</div> </div> </GoodWrapper> ) export default function ListItem({menuList}) { return ( //在遍历的时候以Good组件的形式遍历,去Good中丰富数据的样式 <Wrapper> { menuList.map( (item)=>( <Good goodItem={item} key={item.id}/> ) ) } <div className="tips"> 实际产品以门店供应为准。</div> </Wrapper> ) }
3.4、第三个组件:Footer

- 作为
SPA中离不开的Footer组件,再未来路由跳转的功能中他的地位显著 - 我们通过
react-router-dom中的useLocation,解构出pathname,通过url的变化实现footer的图片切换 - 例如当
pathname=='/home'的时候,展示绿色img(icon),否则展示白色的。 classnames可以用在需要多类名或者动态类名上,和NvBar的实现一样,都是为了实现动态类名,如果没接触过classnames可以看这里classnames的使用import React from 'react' import { Link, useLocation } from 'react-router-dom' import { FooterWrapper } from './style' import classnames from 'classnames' export default function Footer(props) { const { pathname } = useLocation() return ( <FooterWrapper> {/* 为了限制篇幅,这里只展示了两个Link标签 */} <Link to="/account" className={classnames({active:pathname == '/account'})}> {pathname == '/account' ? <img src="https://www-static.chinacdn.starbucks.com.cn/prod/assets/icons/icon-account-active.svg" alt="" className='active'/>: <img src='https://www-static.chinacdn.starbucks.com.cn/prod/assets/icons/icon-account.svg'/> } <span>我的账户</span> </Link> <Link to="/menu" className={classnames({active:pathname == '/menu'})}> {pathname == '/menu' ? <img src="https://www-static.chinacdn.starbucks.com.cn/prod/assets/icons/icon-menu-active.svg" alt="" className='active'/>: <img src='https://www-static.chinacdn.starbucks.com.cn/prod/assets/icons/icon-menu.svg'/> } <span>菜单</span> </Link> </FooterWrapper> ) }
3.5、第四个组件:Menu
- 为了进行规范的
数据管理,我们需要把状态变量都定义在Menu组件,通过父组件统一管理 - Menu组件需要引入
NabBar导航栏组件,ListItem商品组件,搜索框(未封装为组件) - 数据状态主要有
loading,menulist,tab - 使用
UseEffect的第二个参数监听tab的变化,变化就重新执行一次useEffect内部的语句,实现NavBar的切换改变商品的数组export default function Menu() { //菜单组件数据为0时,给weui的Toast组件设置为true const [loading,setLoading]=useState(false); //菜单组件,初始的时候为空,后续通过axios得到 const [menuList,setMenuList] = useState([]) //在NavBar中控制tab切换的状态 const [tab,setTab] = useState("全部") //这个函数会在NavBar中的tab切换后执行,并返回NavBar组件中的tab值 const Fn = (tab) =>{ setTab(tab) } //执行网络请求获取菜单,并且监听tab的变化,变化就重新加载 useEffect(() => { //加载数据前设置Toast状态为true setLoading(true); (async()=>{ const {remainlist} = await getMenuList({tab}); setMenuList(remainlist) })() //加载数据后设置Toast状态未false setLoading(false) },[tab]) return ( <Wrapper> <NavBar tab={tab} Fn={Fn}/> {<Toast show={loading} icon="loading">加载中...</Toast>} {menuList.length>0 ? <ListItem menuList={menuList}/> : <EmptyItem/>} <div className="frap"> <button id="featured-campaign-search" className="button_primary" rel="menu-search-overlay">搜索菜单</button> </div> </Wrapper> ) }
四、总结
4.1、回忆开篇提到的小知识
4.2、组件之间的传值
1. 父传子
- 如下所示
//menu组件 const [tab,setTab] = useState("全部") <NavBar tab={tab}/>//NavBar组件,通过props参数解构出tab export default function NavBar(props) { const {tab} = props
2. 子传父
- 本质上还是父传子,不过是
子组件执行父组件的函数,修改父组件数据属性//menu组件 const [tab,setTab] = useState("全部") const changeTab(tabname){ setTab(tab) } <NavBar Fn={changeTab}/>//NavBar组件 export default function NavBar(props) { const {tab,Fn} = props//得到Menu中定义的tab const changeTab = (tabname)=>{//点击事件执行Menu中的Fn函数 Fn && Fn(tabname); } return ( <Wrapper> <nav className='nav-title'>菜单</nav> <div className="tabs-wrapper"> <ul className='subcategories'> <li className={tab=="全部"?'active':""} onClick={()=>changeTab("全部")}>全部</li> <li className={tab=="饮料"?'active':""} onClick={()=>changeTab("饮料")}>饮料</li> <li className={tab=="美食"?'active':""} onClick={()=>changeTab("美食")}>美食</li> <li className={tab=="咖啡产品"?'active':""} onClick={()=>changeTab("咖啡产品")}>咖啡产品</li> <li className={tab=="商品"?'active':""} onClick={()=>changeTab("商品")}>商品</li> </ul> </div> </Wrapper> ) }
3. 兄弟组件传值
react中我们一般不进行兄弟组件之间的传值,如果确实有这种情况,我们会将数据的状态提升- 例如
a组件与b组件是兄弟组件,我们将数据提升至c组件,c组件作为a,b组件的父组件,通过c组件传给a,b组件,转化为父子组件传值
4.3、切换navbar进行数据的筛选
- 详见3.2
4.4、移动端的不同机型适配(rem)
1. 动态 REM 方案
- 在使用单位控制页面元素大小时,可以使用固定单位
px也可以使用相对单位rem(相对于html的font-size大小)。 - 不同手机的
font-size大小不一样,切换设备时,如果用的是rem则会按照字体大小来进行等比例缩放 - 设计师交付的设计稿宽度一般是
750px,设计稿上一个div的标注尺寸是375px(宽度是设计稿宽度的一半) - 然后再按照
1rem=20px的比例,把所有px都换成rem实现移动端适配var init = function () { //获取当前html的宽度 var clientWidth = document.documentElement.clientWidth || document.body.clientWidth; //移动端宽度如果超过640就按照640来算 if (clientWidth >= 640) { clientWidth = 640; } //得到对应比例的fontsize,这样一个rem就是20px了 var fontSize = 20 / 375 * clientWidth; document.documentElement.style.fontSize = fontSize + "px"; } init(); window.addEventListener("resize", init);
4.5、SPA单页应用Footer的实现
- 在
3.4讲的比较清晰了,样式可以直接去仓库拉取(可以的话给个star)。
五、结尾
至此一个简单的
starbucks菜单组件就到此为止了,如果对您有帮助请麻烦点一个赞并且给项目一个star,后续更新路由及其他组件会再进行文章编辑。
转载自:https://juejin.cn/post/7115209048605065246
