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