超詳細(xì)的React組件設(shè)計(jì)過(guò)程-仿抖音訂單組件
前言
作為數(shù)據(jù)驅(qū)動(dòng)的領(lǐng)導(dǎo)者react/vue等MVVM框架的出現(xiàn),幫我們減少了工作中大量的冗余代碼, 一切皆組件的思想深得人心。組件就是對(duì)一些具有相同業(yè)務(wù)場(chǎng)景和交互模式代碼的抽象,這就需要我們對(duì)組件進(jìn)行規(guī)范的封裝,掌握高質(zhì)量組件設(shè)計(jì)的思路和方法可以幫助我們提高日常的開(kāi)發(fā)效率。筆者將會(huì)通過(guò)實(shí)戰(zhàn)抖音訂單組件詳細(xì)的介紹組件的設(shè)計(jì)思路和方法,對(duì)新手特別友好,希望對(duì)前端新手們和有一定工作經(jīng)驗(yàn)的朋友有一定幫助~
前期準(zhǔn)備
在組件設(shè)計(jì)之前,希望你對(duì)css、js具有一定的基礎(chǔ)。在我們的組件設(shè)計(jì)時(shí)需要用到的開(kāi)源組件庫(kù)有:
(有不了解的小伙伴可以自行查閱資料學(xué)習(xí)一下,在后面用到的時(shí)候我也會(huì)說(shuō)明的)
axios 它是一個(gè)基于 promise 的網(wǎng)絡(luò)請(qǐng)求庫(kù),用于獲取后端數(shù)據(jù),是前端常用的數(shù)據(jù)請(qǐng)求工具;
react-weui、weui weui 是微信官方制作的一個(gè)基礎(chǔ)樣式UI庫(kù),我們可以通過(guò)閱讀官方文檔直接使用里面的樣式,而 react-weui 就是將這些樣式封裝成我們可以直接使用的組件;
styled-components 稱之為css in js,現(xiàn)在正在成為在 React 中設(shè)計(jì)組件樣式的新方法。
另外,我們還用到在線接口工具 faskmock 模擬ajax請(qǐng)求。它更加真實(shí)的模擬了前端開(kāi)發(fā)中后端提供數(shù)據(jù)的方式。
正文
在這我們先來(lái)看看組件實(shí)現(xiàn)后的組件效果:
1. 組件設(shè)計(jì)思路
在這個(gè)組件中我們需要實(shí)現(xiàn)的業(yè)務(wù)有:
(目前我們就暫時(shí)實(shí)現(xiàn)以下效果,該頁(yè)面的其他功能筆者將會(huì)在后期慢慢完善~)
tab切換:
點(diǎn)擊tab,該tab添加上紅色下劃線樣式,并將該tab狀態(tài)下的訂單展示在下方。
設(shè)置loading狀態(tài):
在數(shù)據(jù)還在請(qǐng)求中時(shí),顯示loading圖標(biāo)
搜索訂單:
在當(dāng)前tab下搜索商品標(biāo)題含有輸入內(nèi)容的訂單。
刪除訂單:
刪除指定訂單,由于數(shù)據(jù)是在fastmock中請(qǐng)求得到,因此刪除只相對(duì)于前端。
實(shí)現(xiàn)Empty(空狀態(tài))組件
當(dāng)當(dāng)前狀態(tài)下訂單數(shù)量為 0 時(shí),顯示該組件,否則顯示列表組件。
根據(jù)我們的需求,可以劃分出5個(gè)組件模塊組成整個(gè)頁(yè)面:
頁(yè)面級(jí)別組件<Myorder/>,它是其他組件的父組件;
顯示數(shù)據(jù)列表組件<OrderList/>,單個(gè)數(shù)據(jù)組件<OrderNote/>;
空狀態(tài)組件<EmptyItem/>;
推薦商品列表組件<RecommendList/>。
在<Myoeder/>組件中請(qǐng)求數(shù)據(jù),將對(duì)應(yīng)的數(shù)組數(shù)據(jù)通過(guò)props傳給<OrderList/>組件和<RecommendList/>組件;<OrderList/>組件再將單個(gè)數(shù)據(jù)傳給<OrderNote/>組件。這樣就規(guī)范的完成了父組件請(qǐng)求數(shù)據(jù),子組件搭建樣式的分工合作了。
分析完組件組成接下來(lái)完成組件目錄的搭建:
2. 實(shí)現(xiàn) Myorder 組件
首先我們先根據(jù)需求將組件框架寫(xiě)好,這樣后面寫(xiě)業(yè)務(wù)邏輯會(huì)更清晰:
這個(gè)頁(yè)面級(jí)別組件包括固定在頂部的搜索框+導(dǎo)航欄,以及OrderList和RecommendList組件,因此可以寫(xiě)出如下組件框架:
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>
// 搜索 + 導(dǎo)航欄 部分
<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>待發(fā)貨</li>
<li>待收貨/使用</li>
<li>評(píng)價(jià)</li>
<li>退款</li>
</ul>
</div>
// 訂單列表組件
<OrderList/>
// 推薦列表組件
<RecommendList/>
</OrderWrapper>
)
}
有了這個(gè)框架,我們來(lái)一步步往里面實(shí)現(xiàn)內(nèi)容吧。
2.1 實(shí)現(xiàn)tab切換效果
首先來(lái)完成第一個(gè)需求:當(dāng)點(diǎn)擊某個(gè)tab時(shí),如'待支付',這個(gè)tab要有紅色下劃線效果。實(shí)現(xiàn)原理其實(shí)很簡(jiǎn)單,就是當(dāng)我們觸發(fā)該tab的點(diǎn)擊事件時(shí),就將我們事先寫(xiě)好的active樣式加到該tab上。
這里有兩種方案:
第一種實(shí)現(xiàn)方法是定義一個(gè)狀態(tài)tab來(lái)控制每個(gè)<li>的className的內(nèi)容:
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=='待發(fā)貨'?'active':''} onClick={changeTab.bind(null,'待發(fā)貨')}>待發(fā)貨</li>
<li className={tab=='待收貨/使用'?'active':''} onClick={changeTab.bind(null,'待收貨/使用')}>待收貨/使用</li>
<li className={tab=='評(píng)價(jià)'?'active':''} onClick={changeTab.bind(null,'評(píng)價(jià)')}>評(píng)價(jià)</li>
<li className={tab=='退款'?'active':''} onClick={changeTab.bind(null,'退款')}>退款</li>
</ul>
...
</OrderWrapper>
)
}
這種方法有一個(gè)明顯的缺點(diǎn),就是只能為其添加一個(gè)樣式名,當(dāng)有多個(gè)樣式類名時(shí),就會(huì)出問(wèn)題了,因此可以采用第二種方法。
第二種方法就是用 classnames 了,也是比較推薦的方法,寫(xiě)法也比較簡(jiǎn)單。
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==="待發(fā)貨"})} onClick={changeTab.bind(null,'待發(fā)貨')}>待發(fā)貨</li>
<li className={classnames({active:tab==="待收貨/使用"})} onClick={changeTab.bind(null,'待收貨/使用')}>待收貨/使用</li>
<li className={classnames({active:tab==="評(píng)價(jià)"})} onClick={changeTab.bind(null,'評(píng)價(jià)')}>評(píng)價(jià)</li>
<li className={classnames({active:tab==="退款"})} onClick={changeTab.bind(null,'退款')}>退款</li>
</ul>
...
</OrderWrapper>
)
}
當(dāng)有多個(gè)類名時(shí),這樣添加:
<li className={classnames('test',{active:tab==="全部"})} onClick={changeTab.bind(null,'全部')}>全部</li>
實(shí)現(xiàn)效果如圖:
2.2 獲取數(shù)據(jù)
這里準(zhǔn)備了兩個(gè)接口,用于獲取訂單數(shù)據(jù)和推薦商品數(shù)據(jù)。
為了便于管理,我們將數(shù)據(jù)請(qǐng)求封裝在api文件中:
第一個(gè)接口獲取訂單數(shù)據(jù)。需要根據(jù) tab狀態(tài)篩選獲取的數(shù)據(jù),這一步我們也寫(xiě)在接口文件中:
import axios from 'axios'
// 請(qǐng)求訂單數(shù)據(jù)
export const getOrder = ({tab}) =>
axios
.get('https://www.fastmock.site/mock/759aba4bef0b02794e330cccc1c88555/beers/order')
.then ( res => {
let result=res.data;
if(tab){
switch(tab) {
case "待支付":
result=result.filter(item => item.state=="待支付");
break;
case "待發(fā)貨":
result=result.filter(item => item.state=="待發(fā)貨");
break;
case "待收貨/使用":
result=result.filter(item => item.state=="待收貨/使用");
break;
case "評(píng)價(jià)":
result=result.filter(item => item.state=="評(píng)價(jià)");
break;
case "退款":
result=result.filter(item => item.state=="退款");
break;
default:
break;
}
}
return Promise.resolve({
result
});
}
)
第二個(gè)接口獲取推薦商品數(shù)據(jù):
import axios from 'axios'
// 請(qǐng)求推薦商品數(shù)據(jù)
export const getCommend = () =>
axios.get('https://www.fastmock.site/mock/759aba4bef0b02794e330cccc1c88555/beers/goods')
接口準(zhǔn)備好了,接下來(lái)我們將數(shù)據(jù)分配給子組件,接下來(lái)數(shù)據(jù)如何在頁(yè)面上顯示的任務(wù)就交給子組件<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([]);
// 從接口中獲取推薦商品數(shù)據(jù)
useEffect(()=> {
(async()=> {
const {data} = await getCommend();
setRecommend([...data]);
})()
})
// 從接口中獲取訂單數(shù)據(jù),每次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 實(shí)現(xiàn)搜索功能
搜索功能應(yīng)該在對(duì)應(yīng)的tab下進(jìn)行,因此我們可以將輸入的內(nèi)容設(shè)置為一個(gè)狀態(tài),每次改變就根據(jù)tab內(nèi)容和輸入內(nèi)容重新獲取數(shù)據(jù):
api接口對(duì)訂單數(shù)據(jù)的請(qǐng)求的封裝中增加一個(gè)query限制:
export const getOrder = ({tab,query}) =>
axios
.get('https://www.fastmock.site/mock/759aba4bef0b02794e330cccc1c88555/beers/order')
.then ( res => {
let result=res.data;
if(tab){
switch(tab) {
case "待支付":
result=result.filter(item => item.state=="待支付");
break;
case "待發(fā)貨":
result=result.filter(item => item.state=="待發(fā)貨");
break;
case "待收貨/使用":
result=result.filter(item => item.state=="待收貨/使用");
break;
case "評(píng)價(jià)":
result=result.filter(item => item.state=="評(píng)價(jià)");
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
});
}
)
而在組件的實(shí)現(xiàn)上,由于頁(yè)面沒(méi)有添加點(diǎn)擊搜索的按鈕,如果將input中的value直接和query狀態(tài)綁定的話,每次用戶輸入一個(gè)字就會(huì)進(jìn)行一次查詢,觸發(fā)太頻繁,性能不夠好,用戶體驗(yàn)也不好。所以這里我的想法是每次輸入完按下enter才進(jìn)行搜索
但是React中無(wú)法直接對(duì)input的enter事件進(jìn)行處理。于是我在網(wǎng)上查閱到兩種處理方式,第一種是通過(guò) e.nativeEvent 來(lái)獲取keyCode判斷是否為 13 ,第二中方法是通過(guò)addEventListener注冊(cè)事件來(lái)處理,要慎用。這里采用第一種方法來(lái)實(shí)現(xiàn):
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 設(shè)置loading狀態(tài)
在數(shù)據(jù)請(qǐng)求過(guò)程之,頁(yè)面會(huì)空白,為了提升視覺(jué)上的效果,在這個(gè)時(shí)間段我們就設(shè)置一個(gè)loading樣式,這個(gè)樣式組件我們直接使用reacct-weui的Toast組件。
我們?cè)黾右粋€(gè)loading狀態(tài)來(lái)來(lái)控制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>
)
}
實(shí)現(xiàn)效果如圖:
2.5 實(shí)現(xiàn)Empty(空狀態(tài))組件
空狀態(tài) 組件,顧名思義就是當(dāng)請(qǐng)求到的數(shù)據(jù)為空或者是數(shù)據(jù)長(zhǎng)度為 0 時(shí),就顯示該組件。這個(gè)組件實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,因此這里我們直接寫(xiě)在myorder組件中,用styled-components實(shí)現(xiàn)效果。
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>暫無(wú)訂單</h2>
<p>你還沒(méi)有產(chǎn)生任何訂單</p>
</EmptyItem>
}
...
</OrderWrapper>
)
}
完成上面這些業(yè)務(wù),myorder組件就完成的差不多啦~
3. 實(shí)現(xiàn) OederList 組件
這個(gè)組件只需要將父組件myorder傳進(jìn)來(lái)的數(shù)組數(shù)據(jù)通過(guò) map 分配給 OederNote,另外刪除功能在它的子組件OrderNote上觸發(fā),需要通過(guò)它解構(gòu)出deleteOrder函數(shù)傳給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. 實(shí)現(xiàn) OrderNote 組件
該組件主要負(fù)責(zé)實(shí)現(xiàn)訂單的展示效果,這里只展示部分代碼
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>
)
在這個(gè)組件可以觸發(fā)刪除訂單的業(yè)務(wù),具體如何刪除我們只需要在父組件myOrder實(shí)現(xiàn),然后將函數(shù)傳遞到OrderNote觸發(fā)
在myOrder組件添加deleteOrder函數(shù):
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. 實(shí)現(xiàn) RecommendList 組件
該組件也是對(duì)從父組件Myorder獲取來(lái)的數(shù)據(jù)進(jìn)行展示,主要是做樣式上的功夫。使用多列布局,將頁(yè)面分為兩列,并且不固定每個(gè)數(shù)據(jù)盒子的高度。
最外層列表盒子加上屬性: column-count:2; 將頁(yè)面分為兩列
列表中的每一個(gè)單獨(dú)的小盒子添加屬性:break-inside:avoid; 控制文本塊分解成單獨(dú)的列,以免項(xiàng)目列表的內(nèi)容跨列,破壞整體的布局**
圖片的寬度設(shè)置:width:100%
多列布局注意上面三點(diǎn)就差不多了
作者:前端Q
歡迎關(guān)注微信公眾號(hào) :前端Q