前言
作为数据驱动的领导者react/vue等MVVM框架的出现,帮我们减少了工作中大量的冗余代码, 一切皆组件的思想深得人心。组件就是对一些具有相同业务场景和交互模式代码的抽象,这就需要我们对组件进行规范的封装,掌握高质量组件设计的思路和方法可以帮助我们提高日常的开发效率。笔者将会通过实战抖音订单组件详细的介绍组件的设计思路和方法,对新手特别友好,希望对前端新手们和有一定工作经验的朋友有一定帮助~
前期准备
在组件设计之前,希望你对css、js具有一定的基础。在我们的组件设计时需要用到的开源组件库有:
(有不了解的小伙伴可以自行查阅资料学习一下,在后面用到的时候我也会说明的)
axios 它是一个基于 promise 的网络请求库,用于获取后端数据,是前端常用的数据请求工具;
react-weui 、 weui weui 是微信官方制作的一个基础样式UI库,我们可以通过阅读官方文档直接使用里面的样式,而 react-weui 就是将这些样式封装成我们可以直接使用的组件;
styled-components 称之为 css in js ,现在正在成为在 React 中设计组件样式的新方法。
另外,我们还用到在线接口工具 faskmock 模拟ajax请求。它更加真实的模拟了前端开发中后端提供数据的方式。
实现后的组件效果
在这我们先来看看组件实现后的组件效果:
1. 组件设计思路
在这个组件中我们需要实现的业务有:
(目前我们就暂时实现以下效果,该页面的其他功能笔者将会在后期慢慢完善~)
根据我们的需求,可以划分出5个组件模块组成整个页面:
页面级别组件 <Myorder/> ,它是其他组件的父组件; 显示数据列表组件 <OrderList/> ,单个数据组件 <OrderNote/> ; 空状态组件 <EmptyItem/> ; 推荐商品列表组件 <RecommendList/> 。 在 <Myoeder/> 组件中请求数据,将对应的数组数据通过 props 传给 <OrderList/> 组件和 <RecommendList/> 组件; <OrderList/> 组件再将单个数据传给 <OrderNote/> 组件。这样就规范的完成了父组件请求数据,子组件搭建样式的分工合作了。分析完组件组成接下来完成组件目录的搭建:
2. 实现 Myorder 组件
首先我们先根据需求将组件框架写好,这样后面写业务逻辑会更清晰:
这个页面级别组件包括固定在顶部的搜索框+导航栏,以及 OrderList 和 RecommendList 组件,因此可以写出如下组件框架:
import React from 'react' import OrderList from 'OrderList' import RecommendList from 'RecommendList' import { OrderWrapper } from './style' import fanhui from 'assets/images/fanhui.svg' import gengduo from 'assets/images/gengduo.svg' import sousuo from 'assets/images/sousuo.svg' export default function Myorder() { return ( <OrderWrapper> // 搜索 + 导航栏 部分 <div className="head"> <div className="searchOrder"> <img src={fanhui} alt="返回"/> <div className='searchgroup'> <input placeholder="搜索订单" /> <img className="searchimg" src={sousuo} alt="搜索"/> </div> <img src={gengduo} alt="更多"/> </div> <ul> <li>全部</li> <li>待支付</li> <li>待发货</li> <li>待收货/使用</li> <li>评价</li> <li>退款</li> </ul> </div> // 订单列表组件 <OrderList/> // 推荐列表组件 <RecommendList/> </OrderWrapper> ) }
有了这个框架,我们来一步步往里面实现内容吧。
2.1 实现tab切换效果
首先来完成第一个需求:当点击某个 tab 时,如'待支付',这个 tab 要有红色下划线效果。实现原理其实很简单,就是当我们触发该 tab 的点击事件时,就将我们事先写好的 active 样式加到该 tab 上。
这里有两种方案:
import React,{ useState} from 'react' import { OrderWrapper } from './style' export default function Myorder() { const [tab,setTab] = useState('全部'); const changeTab= (target) => { setTab(target); } return ( <OrderWrapper> ... <ul> <li className={tab=='全部'?'active':''} onClick={changeTab.bind(null,'全部')}>全部</li> <li className={tab=='待支付'?'active':''} onClick={changeTab.bind(null,'待支付')}>待支付</li> <li className={tab=='待发货'?'active':''} onClick={changeTab.bind(null,'待发货')}>待发货</li> <li className={tab=='待收货/使用'?'active':''} onClick={changeTab.bind(null,'待收货/使用')}>待收货/使用</li> <li className={tab=='评价'?'active':''} onClick={changeTab.bind(null,'评价')}>评价</li> <li className={tab=='退款'?'active':''} onClick={changeTab.bind(null,'退款')}>退款</li> </ul> ... </OrderWrapper> ) }
这种方法有一个明显的缺点,就是只能为其添加一个样式名,当有多个样式类名时,就会出问题了,因此可以采用第二种方法。
第二种方法就是用 classnames 了,也是比较推荐的方法,写法也比较简单。import classnames from 'classnames' import { OrderWrapper } from './style' export default function Myorder() { const [tab,setTab] = useState('全部'); const changeTab= (target) => { setTab(target); } return ( <OrderWrapper> ... <ul> <li className={classnames({active:tab==="全部"})} onClick={changeTab.bind(null,'全部')}>全部</li> <li className={classnames({active:tab==="待支付"})} onClick={changeTab.bind(null,'待支付')}>待支付</li> <li className={classnames({active:tab==="待发货"})} onClick={changeTab.bind(null,'待发货')}>待发货</li> <li className={classnames({active:tab==="待收货/使用"})} onClick={changeTab.bind(null,'待收货/使用')}>待收货/使用</li> <li className={classnames({active:tab==="评价"})} onClick={changeTab.bind(null,'评价')}>评价</li> <li className={classnames({active:tab==="退款"})} onClick={changeTab.bind(null,'退款')}>退款</li> </ul> ... </OrderWrapper> ) }
当有多个类名时,这样添加:
<li className={classnames('test',{active:tab==="全部"})} onClick={changeTab.bind(null,'全部')}>全部</li>
实现效果如图:
2.2 获取数据
这里准备了两个接口,用于获取 订单数据 和 推荐商品数据 。
为了便于管理,我们将数据请求封装在api文件中:
import axios from 'axios' // 请求订单数据 export const getOrder = ({tab}) => axios .get('https://HdhCmsTestfastmock.site/mock/759aba4bef0b02794e330cccc1c88555/beers/order') .then ( res => { let result=res.data; if(tab){ switch(tab) { case "待支付": result=result.filter(item => item.state=="待支付"); break; case "待发货": result=result.filter(item => item.state=="待发货"); break; case "待收货/使用": result=result.filter(item => item.state=="待收货/使用"); break; case "评价": result=result.filter(item => item.state=="评价"); break; case "退款": result=result.filter(item => item.state=="退款"); break; default: break; } } return Promise.resolve({ result }); } )第二个接口 获取推荐商品数据 :
import axios from 'axios' // 请求推荐商品数据 export const getCommend = () => axios.get('https://HdhCmsTestfastmock.site/mock/759aba4bef0b02794e330cccc1c88555/beers/goods')
接口准备好了,接下来我们将数据分配给子组件,接下来数据如何在页面上显示的任务就交给子组件 <OrderList/> 和 <Recommend/> 完成
import React,{useEffect, useState} from 'react' import { OrderWrapper } from './style' import OrderList from './OrderList' import RecommendList from './RecommendList' export default function Myorder() { const [list,setList] =useState([]); const [recommend,setRecommend] = useState([]); // 从接口中获取推荐商品数据 useEffect(()=> { (async()=> { const {data} = await getCommend(); setRecommend([...data]); })() }) // 从接口中获取订单数据,每次tab切换都重新拉取 useEffect(()=>{ (async()=>{ const {result} = await getOrder({tab}); setList([ ...result ]) })() },[tab]) return ( <OrderWrapper> ... {list.length>0 && <OrderList list={list}/>} {recommend.length>0 && <RecommendList recommend={recommend}/>} </OrderWrapper> ) }
2.3 实现搜索功能
搜索功能应该在对应的 tab 下进行,因此我们可以将输入的内容设置为一个 状态 ,每次改变就根据 tab 内容和输入内容重新获取数据:
api接口对订单数据的请求的封装中增加一个 query 限制:
export const getOrder = ({tab,query}) => axios .get('https://HdhCmsTestfastmock.site/mock/759aba4bef0b02794e330cccc1c88555/beers/order') .then ( res => { let result=res.data; if(tab){ switch(tab) { case "待支付": result=result.filter(item => item.state=="待支付"); break; case "待发货": result=result.filter(item => item.state=="待发货"); break; case "待收货/使用": result=result.filter(item => item.state=="待收货/使用"); break; case "评价": result=result.filter(item => item.state=="评价"); break; case "退款": result=result.filter(item => item.state=="退款"); break; default: break; } } if(query) { result = result.filter(item => item.title.includes(query)); } return Promise.resolve({ result }); } )
而在组件的实现上,由于页面没有添加点击搜索的按钮,如果将 input 中的 value 直接和 query 状态绑定的话,每次用户输入一个字就会进行一次查询,触发太频繁,性能不够好,用户体验也不好。
所以这里我的想法是每次输入完按下 enter 才进行搜索
但是React中无法直接对 input 的 enter 事件进行处理。于是我在网上查阅到两种处理方式,第一种是通过 e.nativeEvent 来获取 keyCode 判断是否为 13 ,第二中方法是通过 addEventListener 注册事件来处理,要慎用。
这里采用第一种方法来实现:
import React,{useState} from 'react' import { OrderWrapper } from './style' export default function Myorder() { const [query,setQuery] = useState(''); const handleEnterKey = (e) => { if(e.nativeEvent.keyCode === 13){ setQuery(e.target.value); } } return ( <OrderWrapper> ... <input placeholder="搜索订单" onKeyPress={handleEnterKey} /> ... </div> </OrderWrapper> ) }
2.4 设置loading状态
在数据请求过程之,页面会空白,为了提升视觉上的效果,在这个时间段我们就设置一个 loading 样式,这个样式组件我们直接使用 reacct-weui 的 Toast 组件。
我们增加一个 loading 状态来来控制 Toast 的显示。
import React,{useEffect, useState} from 'react' import { OrderWrapper } from './style' import WeUI from 'react-weui' const { Toast } = WeUI; export default function Myorder() { const [loading,setLoading]=useState(false); useEffect(()=>{ setLoading(true); (async()=>{ const {result} = await getOrder({tab}); setList([ ...result ]) setLoading(false); })() },[tab]) return ( <OrderWrapper> ... <Toast show={loading} icon="loading">加载中...</Toast> { list.length>0 && <OrderList list={list}} ... <OrderWrapper> ) }
实现效果如图:
2.5 实现Empty(空状态)组件
空状态 组件,顾名思义就是当请求到的数据为空或者是数据长度为 0 时,就显示该组件。这个组件实现起来比较简单,因此这里我们直接写在 myorder 组件中,用 styled-components 实现效果。
import React,{useEffect, useState} from 'react' import { OrderWrapper,EmptyItem } from './style' import OrderList from './OrderList' import empty from 'assets/images/empty.png' export default function Myorder() { const [list,setList] = useState([]); ... return ( <OrderWrapper> ... {list.length>0&&<OrderList list={list} deleteOrder={deleteOrder}/>} {list.length==0&&loading==false&& <EmptyItem> <h3>美好生活 触手可得</h3> <img src={empty} /> <h2>暂无订单</h2> <p>你还没有产生任何订单</p> </EmptyItem> } ... </OrderWrapper> ) }
完成上面这些业务, myorder 组件就完成的差不多啦~
3. 实现 OederList 组件
这个组件只需要将父组件 myorder 传进来的数组数据通过 map 分配给 OederNote ,另外删除功能在它的子组件 OrderNote 上触发,需要通过它解构出 deleteOrder 函数传给 OrderNote
import React from 'react' import { OrderListWrapper } from './style' export default function OrderList({list,deleteOrder}) { return ( <OrderListWrapper> <h3>美好生活 触手可得</h3> { list.map(item => ( <OrderNote key={item.id} data={item} deleteOrder={()=>deleteOrder(item.id)}/> )) } </OrderListWrapper> ) }
4. 实现 OrderNote 组件
该组件主要负责实现订单的展示效果,这里只展示部分代码
import React from 'react' import { NoteWrapper } from './style' const OrderNote = (props) => { const { data } =props; const { deleteOrder } =props return ( <NoteWrapper> ... <div className="btngroup"> <button onClick={deleteOrder}>删除订单</button> <button>查看相似</button> </div> </div> </NoteWrapper> )
在这个组件可以触发删除订单的业务,具体如何删除我们只需要在父组件 myOrder 实现,然后将函数传递到 OrderNote 触发
在 myOrder 组件添加 deleteOrder 函数:
import React from 'react' import OrderList from './OrderList' export default function Myorder() { const deleteOrder = (id) => { setList(list.filter(order => order.id!==id)); } ... return ( <OrderWrapper> ... {list.length>0&&<OrderList list={list} deleteOrder={deleteOrder}/>} ... </OrderWrapper> ) }
5. 实现 RecommendList 组件
该组件也是对从父组件 Myorder 获取来的数据进行展示,主要是做样式上的功夫。使用多列布局,将页面分为两列,并且不固定每个数据盒子的高度。
最外层列表盒子加上属性: column-count:2; 将页面分为两列 列表中的每一个单独的小盒子添加属性: break-inside:avoid; 控制文本块分解成单独的列,以免项目列表的内容跨列,破坏整体的布局** 图片的宽度设置: width:100%多列布局注意上面三点就差不多了
最后
以上就是笔者目前完成整个组件设计、封装的过程啦,后面会去继续学习下拉刷新、上拉加载等功能,慢慢完善这个组件。
源码地址: cool-g/react-reportPage: 仿抖音我的订单组件 (github测试数据)
gitpage地址(直接查看页面效果): Vite App (cool-g.github.io)
更多关于React抖音订单组件设计的资料请关注其它相关文章!
查看更多关于React组件设计过程之仿抖音订单组件的详细内容...