手把手带你做一个React入门实战demo:星巴克菜单组件/移动端适配/Tab切换

我正在参与掘金技术社区创作者签约计划招募活动

一、前言

本项目是为了给react新手入门做的一个仿星巴克菜单组件demo,如果能帮助到您,还请给一个小小的点赞

1.1、项目展示

ezgif.com-gif-maker.gif

项目开源地址github:github.com/Youngzx88/s…

项目开源地址:gitee.com/yyyyyyyyzx/…

1.1、可以学到的小知识

  1. 组件之间的传值(父传子,子传父)
  2. 切换navbar进行数据的筛选
  3. 移动端的不同机型适配(rem)
  4. 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. 切分页面

  • 图片.png
  • 这样一个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. 数据准备

3.2、第一个组件:NavBar

  • 图片.png

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

  • 图片.png
  • 这个组件的功能主要是实现接口数据的展示,并封装样式输出
  • 我们可以在遍历的时候以组件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">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;实际产品以门店供应为准。</div>
            </Wrapper>
            )
    }
    

3.4、第三个组件:Footer

  • 图片.png
  • 作为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(相对于htmlfont-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,后续更新路由及其他组件会再进行文章编辑。

项目开源地址github:github.com/Youngzx88/s…

项目开源地址:gitee.com/yyyyyyyyzx/…

猜你喜欢

转载自juejin.im/post/7115209048605065246