React組件設計過程之仿抖音訂單組件

前言

作為數據驅動的領導者react/vue等MVVM框架的出現,幫我們減少瞭工作中大量的冗餘代碼, 一切皆組件的思想深得人心。組件就是對一些具有相同業務場景和交互模式代碼的抽象,這就需要我們對組件進行規范的封裝,掌握高質量組件設計的思路和方法可以幫助我們提高日常的開發效率。筆者將會通過實戰抖音訂單組件詳細的介紹組件的設計思路和方法,對新手特別友好,希望對前端新手們和有一定工作經驗的朋友有一定幫助~

前期準備

在組件設計之前,希望你對css、js具有一定的基礎。在我們的組件設計時需要用到的開源組件庫有:
(有不瞭解的小夥伴可以自行查閱資料學習一下,在後面用到的時候我也會說明的)

axios 它是一個基於 promise 的網絡請求庫,用於獲取後端數據,是前端常用的數據請求工具;

react-weuiweui weui 是微信官方制作的一個基礎樣式UI庫,我們可以通過閱讀官方文檔直接使用裡面的樣式,而 react-weui 就是將這些樣式封裝成我們可以直接使用的組件;

styled-components 稱之為css in js,現在正在成為在 React 中設計組件樣式的新方法。

另外,我們還用到在線接口工具 faskmock 模擬ajax請求。它更加真實的模擬瞭前端開發中後端提供數據的方式。

實現後的組件效果

在這我們先來看看組件實現後的組件效果:

1. 組件設計思路

在這個組件中我們需要實現的業務有:
(目前我們就暫時實現以下效果,該頁面的其他功能筆者將會在後期慢慢完善~)

  • tab切換:點擊tab,該tab添加上紅色下劃線樣式,並將該tab狀態下的訂單展示在下方。
  • 設置loading狀態:在數據還在請求中時,顯示loading圖標
  • 搜索訂單:在當前tab下搜索商品標題含有輸入內容的訂單。
  • 刪除訂單:刪除指定訂單,由於數據是在fastmock中請求得到,因此刪除隻相對於前端。
  • 實現Empty(空狀態)組件當當前狀態下訂單數量為 0 時,顯示該組件,否則顯示列表組件。

根據我們的需求,可以劃分出5個組件模塊組成整個頁面:

  • 頁面級別組件<Myorder/>,它是其他組件的父組件;
  • 顯示數據列表組件<OrderList/>,單個數據組件<OrderNote/>
  • 空狀態組件<EmptyItem/>
  • 推薦商品列表組件<RecommendList/>
  • <Myoeder/>組件中請求數據,將對應的數組數據通過props傳給<OrderList/>組件和<RecommendList/>組件;<OrderList/>組件再將單個數據傳給<OrderNote/>組件。這樣就規范的完成瞭父組件請求數據,子組件搭建樣式的分工合作瞭。

分析完組件組成接下來完成組件目錄的搭建:

2. 實現 Myorder 組件

首先我們先根據需求將組件框架寫好,這樣後面寫業務邏輯會更清晰:

這個頁面級別組件包括固定在頂部的搜索框+導航欄,以及OrderListRecommendList組件,因此可以寫出如下組件框架:

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上。
這裡有兩種方案:

  • 第一種實現方法是定義一個狀態tab來控制每個<li>className的內容:
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文件中:

  • 第一個接口獲取訂單數據。需要根據 tab狀態篩選獲取的數據,這一步我們也寫在接口文件中:
import axios from 'axios'
// 請求訂單數據
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 "待發貨":
                        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://www.fastmock.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://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 "待發貨":
                        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中無法直接對inputenter事件進行處理。於是我在網上查閱到兩種處理方式,第一種是通過 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-weuiToast組件。
我們增加一個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.com)

gitpage地址(直接查看頁面效果):Vite App (cool-g.github.io)

更多關於React抖音訂單組件設計的資料請關註WalkonNet其它相關文章!

推薦閱讀: