首頁 > 軟體

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

2022-07-05 18:01:49

前言

作為資料驅動的領導者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抖音訂單元件設計的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com