超詳細(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>美好生活&nbsp;&nbsp;觸手可得</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>美好生活&nbsp;&nbsp;觸手可得</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