這都拿不下你?React + Redux 讓你一眼就愛上的嗶哩嗶哩會員購

語言: CN / TW / HK

theme: cyanosis highlight: vs2015


我正在參加「創意開發 投稿大賽」詳情請看:掘金創意開發大賽來了!

繼上一篇文章React專案開發-仿嗶哩嗶哩移動端首頁之後,我經過一段時間的學習(主要是對Redux的學習)之後,寫下了這篇關於 React + Redux 專案的文章,新增了會員購介面,對首頁進行了改進,優化了使用者體驗,使用 Redux 對狀態統一進行管理。

前言

1. Redux 概述

Redux 是一個使用叫做 action 的事件來管理和更新應用狀態的模式和工具庫,它以集中式Store(centralized store)的方式對整個應用中使用的狀態進行集中管理,其規則確保狀態只能以可預測的方式更新。

2. 為什麼要使用 Redux?

Redux 提供的模式和工具讓我們更容易理解應用程式中的狀態何時、何地、為什麼以及如何更新,以及當這些更改發生時應用程式邏輯將如何表現。

3. 我們應該如何使用 Redux?

Redux 的應用場景:

  • 在應用的大量地方,都存在大量的狀態
  • 應用狀態會隨著時間的推移而頻繁更新
  • 更新該狀態的邏輯可能很複雜
  • 中型和大型程式碼量的應用,很多人協同開發

Redux 工作流

為了更好的理解,我們把 Redux 工作流比作圖書館借書流程,當我們(Component)向管理員(Store)發出一個借書行為(Action)時,管理員接收到後,對照借書記錄本(Reducers)檢視,管理員拿到新書(一個新的狀態)後交給我們。

image.png

專案預覽:Github Pages

專案準備

安裝依賴

在使用 redux 之前,我們需要在之前的基礎上安裝以下依賴(預設安裝最新版本):

npm i redux npm i react-redux npm i redux-thunk npm i redux-logger

redux-thunk 主要的功能就是讓我們可以 dispatch 一個函式,applyMiddlewareRedux 的一個原生方法,可將所有中介軟體組成一個數組,依次執行。

```js import { createStore, compose, applyMiddleware } from 'redux' import reducer from './reducer' import thunk from 'redux-thunk' // 非同步資料管理 import logger from 'redux-logger' // 讓 redux 除錯更優秀

const composeEnhancers = window.REDUX_DEVTOOLS_EXTENSION_COMPOSE || compose // 啟用 redux devtools

const store = createStore(reducer, // 合併成一箇中間件物件 compose( composeEnhancers(applyMiddleware(thunk)), applyMiddleware(logger) ) )

export default store ```

安裝外掛

為了能夠看到效果,我們還需在瀏覽器中(建議使用Chrome)安裝外掛:Redux DevTools,Redux DevTools外掛下載地址

當我們激活了 redux devtools 後,切換到瀏覽器的 Redux 介面可以看到當前倉庫狀態:

Snipaste_2022-07-24_20-52-39.png

redux-logger 會在 dispatch 改變倉庫狀態的時候打印出舊的倉庫狀態、當前觸發的action以及新的倉庫狀態。

Snipaste_2022-07-24_20-54-54.png

實現功能

使用者體驗方面

1. 瀑布流佈局 + 圖片懶載入

multi-column 佈局中子元素的排列順序是先從上往下再從左至右的,讓上下相鄰的子元素分開使用 margin-bottom 即可。

  • 瀑布流的優點如下:
  1. 節省空間,外表美觀,更有藝術性。
  2. 對於觸屏裝置非常友好,通過向上滑動瀏覽。
  3. 使用者瀏覽時的觀賞和思維不容易被打斷,留存更容易。

實現程式碼:

css /* 父容器 */ .container { column-count: 2; // 兩列布局 column-gap: 10px; // 列間距為 10px } /* 子元素 */ .good-box { width: 100%; break-inside: avoid; // 元素不能中斷,auto 可以中斷 }

實現效果:

Snipaste_2022-07-24_23-58-20.png

當從遠端請求過來的圖片還沒加載出來時,使用預設圖片進行佔位,優化使用者體驗。這裡我勾選瀏覽器中的禁用快取模擬了一下效果:

Snipaste_2022-07-25_00-16-13.png

2. 頁面切換

頁面切換的效果我使用了 CSSTransition 對子元素進行包裹,它會將過渡型別給到子元素,新增動畫效果。

實現程式碼:

```jsx import { CSSTransition } from 'react-transition-group' ...

<CSSTransition in={show} // 控制動畫的開關 timeout={300} // 動畫執行時間 appear={true} // 第一次載入該元件時啟用相應的動畫渲染 classNames="fly" unmountOnExit // 動畫效果消失時,該標籤會從 dom 樹上移除

... ```

```js import styled from "styled-components"

export const Wrapper = styled.div... /* CSSTransition 過度型別給children */ &.fly-enter,&.fly-appear { opacity: 0; /* 啟用GPU加速 */ transform: translate3d(100%, 0, 0); } &.fly-enter-active, &.fly-apply-active { opacity: 1; transition: all .3s; transform: translate3d(0, 0, 0); } &.fly-exit { opacity: 1; transform: translate3d(0,0,0) } &.fly-exit-active { opacity: 0; transition: all .3s; transform: translate3d(100%, 0, 0); } ```

效果如下:

chrome-capture-2022-6-25.gif

3. 載入動畫

當我們首次進入到首頁或會員購頁面時,圖片資源是不能瞬間得到的,這裡我使用了 antd-mobile 的動態骨架屏,讓頁面更豐富,填補了等待時間段,提升使用者體驗。

在搜尋介面,我仿造了神三元寫的 loading 元件,這是一個在等待請求過程中的動畫效果。loading 動畫主要實現程式碼如下:

```jsx import React from 'react'; import styled, { keyframes } from 'styled-components';

const loading = keyframes0%, 100% { transform: scale(0.0); } 50% { transform: scale(1.0); } const LoadingWrapper = styled.div>div { position: absolute; top: 0; left: 0; right: 0; bottom: 0; margin: auto; width: 60px; height: 60px; opacity: 0.6; border-radius: 50%; background-color: rgba(0, 150, 250, 0.8); // 這裡可以選擇自己喜歡的顏色 animation: ${loading} 1.4s infinite ease-in; // 動畫持續時間 } >div:nth-child(2) { animation-delay: -0.7s; // 跳過 0.7s 進入動畫週期 }

function Loading() { return (

); }

export default React.memo(Loading); ```

效果如下:

chrome-capture-2022-6-25.gif

業務方面

1. 商品收藏

chrome-capture-2022-6-24.gif

這裡可能會遇到的問題,點選收藏某件商品時,把列表中的所有商品都收藏了,解決方法:

把商品元件進行單獨封裝作為子元件,父元件將 good 傳遞給子元件,子元件拿到單獨的 id,在進行之後的操作時,就不會對其它的子元件造成影響。

```jsx import React from 'react' import propTypes from "prop-types"; import { Wrapper } from './style' import GoodsItem from '@/components/GoodsItem';

export default function GoodsList({goodsList}) {

return (

{ goodsList && goodsList.map(good => ( )) }
) }

GoodsList.propTypes = { goodsList: propTypes.array.isRequired } ```

子元件 GoodsItem 中收藏效果的實現程式碼如下:

```jsx import React, { useState } from "react" import classnames from 'classnames'

const GoodsItem = ({good}) => { const [isColl, setIsColl] = useState(false) // 定義收藏狀態

const changeColl = () => { // 對狀態進行取反 setIsColl(!isColl) }

return (

...
... {/ 當 isColl 為 true 時,使用 classnames 新增相應的樣式,否則為預設樣式 /} {'icon-aixin1': isColl}, {'active': isColl} )} onClick={() => changeColl()} > {/ isColl 為 true 時,收藏量+1,否則不變 /} {isColl ? good.collection + 1 : good.collection}
) }

// 效能優化 export default React.memo(GoodsItem) ```

2. 防抖搜尋功能

首頁搜尋功能由父元件 HomeSearch 和子元件 SearchBox 實現。

首頁搜尋

chrome-capture-2022-6-24 (1).gif

會員購搜尋功能的功能由父元件 VipSearch 和子元件 SearchBox 實現。

會員購搜尋

chrome-capture-2022-6-24 (2).gif

在搜尋上我加上了防抖功能,防抖函式和其他函式放到 util 資料夾下的 index.js 下作為工具使用。

js // 防抖函式 export const debounce = (func, delay) => { let timer return function (...args) { if(timer) { clearTimeout(timer) } timer = setTimeout(() => { func.apply(this, args) clearTimeout(timer) }, delay) } }

在子元件 SearchBox 中修改 query,進行防抖處理,每隔500毫秒執行一次 handleQuery 去更新父元件 VipSearch 中的 query,並通過 dispatch 對狀態進行修改。

jsx // useMomo 可以快取 上一次函式計算的結果 let handleQueryDebounce = useMemo(() => { return debounce(handleQuery, 500) // 每隔 0.5s 執行一次 }, [handleQuery]) // 使用 useEffect 去更新 useEffect(() => { handleQueryDebounce(query) }, [query])

```jsx // 父元件 const VipSearch = (props) => { ... // 輸入時每隔 0.5s 執行一次 useEffect(() => { if (query.trim()) { changeEnterLoadingDispatch(true) getGoodsListDispatch(query) } }, [query]) // 對商品標題進行模糊查詢,將搜尋到的商品進行渲染 const renderGoodsList = () => { return (

商品列表

{ goodsList.filter(good => good.title.indexOf(query) != -1 ).map(good => { return ( ) }) }
) }

return ( navigate(-1)}>取消 ... { enterLoading && } ) }

const mapStateToProps = (state) => { return { enterLoading: state.vipsearch.enterLoading, goodsList: state.vipsearch.goodsList } }

const mapDispatchToProps = (dispatch) => { return { changeEnterLoadingDispatch(data) { dispatch(changeEnterLoading(data)) }, getGoodsListDispatch(query) { dispatch(getGoodsList(query)) } } }

export default connect(mapStateToProps, mapDispatchToProps)(React.memo(VipSearch)) ```

優化

1. 封裝網路請求

api 資料夾下,新添加了 config.js 檔案,用來對物件 axiosInstance 進行封裝,當介面數量較多時,能夠減少程式碼量,使頁面更簡潔:

```js import axios from 'axios' export const baseUrl = "https://www.fastmock.site/mock/059647e88be0d33ef58d6ab4bf009dd9/bilibili" // 單例設計模式 const axiosInstance = axios.create({ baseURL: baseUrl })

// 新增響應攔截,拿到資料時對資料做處理,或丟擲錯誤 axiosInstance.interceptors.response.use( res => res.data, err => { console.log(err, '網路錯誤~') } )

export { axiosInstance } ```

2. 骨架屏佔位

因為大多數圖片資源是從 fastmock 中請求過來的,受網路影響需一些時間,使用者在等待的過程中頁面出現空白狀態很影響體驗,引入骨架屏讓頁面更豐富,填補了等待時間段,優化了使用者體驗。此專案中我使用了 antd-mobile 中的動態骨架屏,Skeleton 骨架屏

3. 圖片懶載入

圖片懶載入也叫“按需載入”,也就是當圖片資源出現在視口區域內,才會被載入,使用懶載入能大大節省網站的流量,對於有大量圖片資源的網站來說顯得尤為重要。

這裡我使用了 LazyLoad,當網路圖片還沒加載出來時,使用本地預設圖片進行佔位,主要程式碼如下:

```jsx import LazyLoad from 'react-lazyload' import bilibili from '@/assets/images/bilibili.jpeg'

... <LazyLoad placeholder={}

... ```

4. memo效能優化

如果你的元件在相同 props 的情況下渲染相同的結果,那麼你可以通過將其包裝在 React.memo 中呼叫,以此通過記憶元件渲染結果的方式來提高元件的效能表現。這意味著在這種情況下,React 將跳過渲染元件的操作並直接複用最近一次渲染的結果。但React.memo 僅檢查 props 的變更。

5. 全域性樣式風格檔案

在專案開發過程中,我們在不同的頁面中可能會用到相同的樣式,例如背景顏色,字型大小,邊框等等,將相同的樣式抽離出來放到 assets 資料夾下的 global-style.js 中,便於對樣式進行統一管理。

js export default { "background-color": "rgba(50, 50, 50, 0.06)", "search_bar-color": "rgba(50, 50, 50, 0.08)", "border-color": "rgba(50, 50, 50, 0.2)", "loading-color": "rgba(0, 150, 250, 0.8)" }

定義了全域性樣式風格檔案後,我們就可以在其他的樣式檔案中進行引用,如下:

```js import styled from "styled-components" import style from '@/assets/global-style'

export const Wrapper = styled.divbackground: ${style["background-color"]}; ... ```

最後

在這個專案中,我借鑑了神三元大佬的網易雲音樂專案中的 CSSTransition 元件, loading 元件和 debounce 防抖函式等,專案地址如下:github原始碼地址,如果有興趣的小夥伴也可以去瞧瞧他寫的掘金小冊React Hooks 與 Immutable 資料流實戰。 本專案在後期仍會繼續改進,實現更多功能,謝謝大家!未完待續......

原始碼地址:bilibili-page

專案預覽:GitHub Pages