一、前言
本项目是为了给
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
,后续更新路由及其他组件会再进行文章编辑。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。