react后臺系統(tǒng)最佳實踐示例詳解
一、中后臺系統(tǒng)的技術棧選型
本文主要講三塊內(nèi)容:中后臺系統(tǒng)的技術棧選型、hooks時代狀態(tài)管理庫的選型以及hooks的使用問題與解決方案。
1. 要做什么
我們的目標是搭建一個適用于公司內(nèi)部中后臺系統(tǒng)的前端項目最佳實踐。
2. 要求
由于業(yè)務需求比較多,一名開發(fā)人員需要負責幾個后臺系統(tǒng)。所以項目最佳實踐的要求按重要性排行:
1、開發(fā)效率。
2、可維護性。
3、性能。
總之,開發(fā)的高效率跟簡單的代碼結(jié)構是比較側(cè)重的兩點。
3. 技術棧怎么選
由于我司前端技術棧主要使用React,所以基礎框架采用React跟React-router。項目開發(fā)內(nèi)容主要是做中后臺系統(tǒng)頁面,于是選擇antd作為系統(tǒng)的UI框架。然后ahooks提供了useAntdTable方法可以幫助我們節(jié)省二次封裝的工作量,所以采用ahooks作為項目主要使用的hooks庫。最后考慮到開發(fā)效率以及性能,狀態(tài)管理庫則采用MobX。
下面詳細說一下狀態(tài)管理庫的選型過程。
二、hooks時代狀態(tài)管理庫的選型
如果使用了ahooks或者React-Query這類帶數(shù)據(jù)請求方案的hooks庫,那已經(jīng)分擔了狀態(tài)管理庫很大一部分工作了。剩下的部分是頁面的交互狀態(tài)處理問題,主要是解決跨組件通信的問題。
目前調(diào)研的狀態(tài)管理方案有以下幾種:
context
首先考慮的是不引入任何狀態(tài)管理庫,直接使用React框架提供的context方法。
React context表面上使用起來很方便,只要定義一個provider并傳入數(shù)據(jù),使用的時候用useContext獲取對應的值即可。
但這個方案需要開發(fā)者考慮如何處理組件重復渲染的問題,需要開發(fā)者考慮是通過手動拆分provider的數(shù)據(jù)還是使用memo、useMemo緩存組件的方案(詳情見這里)。
總的來說解決起來還是比較麻煩的,每次添加狀態(tài)都要檢查這個值是否要拆分、是否頻繁更新以及怎么組織組件比較合理等問題。
總結(jié):React context開發(fā)效率不高、后期維護麻煩。
// 需要拆分狀態(tài)
<UserContext.Provider value={userData}>
<MenuContext.Provider value={menuData}>
{props.children}
</MenuContext.Provider>
</UserContext.Provider>
// 需要緩存組件
useMemo(() => <Component value={a} />, [a])
redux
接下來是目前React狀態(tài)管理庫中下載量最高的redux。
redux這個方案首先要吐槽的是其繁瑣的寫法。每次使用的時候都要煩惱action怎么取名;使用reducer時要寫一大堆擴展運算符,而且一個請求至少要寫三個狀態(tài)(發(fā)送請求、請求成功、請求失敗);異步用thunk會被嫌棄不夠優(yōu)雅,而saga的API又多generator寫法又不好用。
官方的推出的Redux Toolkit框架解決了上面說的action的命名問題,還有reducer的要寫一堆擴展運算符的問題。但狀態(tài)顆粒度太細的問題還是存在,saga的寫法也還是沒變。
如果結(jié)合ahooks的話剛好是可以把saga節(jié)省掉,但用了這些請求庫之后redux鼓吹的狀態(tài)跟蹤的優(yōu)點也就消失了大半~~(雖然感覺這個功能也沒啥作用)~~。只是單純解決跨組件通信的話引入Redux Toolkit又感覺太重了,而
且對比其他狀態(tài)庫Redux Toolkit使用起來還是不夠簡便。
總結(jié):redux是真的繁瑣繁瑣繁瑣。
// 代碼來源網(wǎng)上的[案例](https://codesandbox.io/s/react-ts-redux-toolkit-saga-knq31?file=/src/api/user/userSlice.ts:1752-1761)
// 這一坨代碼實現(xiàn)的功能隨便換個庫就只要幾行就搞定
export const createSagaAction = <
PendingPayload = void,
FulfilledPayload = unknown,
RejectedPayload = Error
>(typePrefix: string): SagaAction<PendingPayload, FulfilledPayload, RejectedPayload> => {
return {
request: createAction<PendingPayload>(`${typePrefix}/request`),
fulfilled: createAction<FulfilledPayload>(`${typePrefix}/fulfilled`),
rejected: createAction<RejectedPayload>(`${typePrefix}/rejected`),
}
}
export const fetchUserAction = createSagaAction<
User['id'],
User
>('user/fetchUser');
export function* fetchUser(action: PayloadAction<User['id']>) {
try {
const user = yield call(getUser, action.payload);
yield put(fetchUserAction.fulfilled(user));
} catch (e) {
yield put(fetchUserAction.rejected(e));
}
}
export function* userSaga() {
yield takeEvery(fetchUserAction.request.type, fetchUser);
}
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: (builder) => (
builder
.addCase(fetchUserAction.request, (state) => {
state.isLoading = true;
})
.addCase(fetchUserAction.fulfilled, (state, action) => {
state.isLoading = false;
state.user = action.payload;
})
.addCase(fetchUserAction.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
})
)
});
recoil
官方還推薦了一個叫recoil的狀態(tài)管理庫,使用了下感覺也不夠簡便。
定義狀態(tài)有兩個常用的api:atom跟selector。atom每次使用都要寫key,selector用著感覺也有點冗余。調(diào)用的api還分useRecoilState跟useRecoilValue,從簡便性來說被下面要講的zustand完爆。
然后這個框架本身也比較新,npm下載量也比zustand要低不少。
總結(jié):簡便性被zustand完爆,下載量不高。
// 定義分atom跟selector
const a = atom({
key: "a",
default: []
});
const b = selector({
key: "b",
get: ({ get }) => {
const list = get(a);
return Math.random() > 0.5 ? list.slice(0, list.length / 2) : list;
}
});
// 調(diào)用則區(qū)分useRecoilValue、useRecoilState
const list = useRecoilValue(b);
const [value, setValue] = useRecoilState(a);
zustand
然后到了勢頭挺猛的zustand,npm的下載量已經(jīng)能趕上MobX了。趨勢對比基本的調(diào)用真的挺簡潔的,通過create定義store,使用的時候直接調(diào)用就好。用起來比recoil方便多了。
但是呢zustand的狀態(tài)都是不可變,getState時跟redux一樣要用到很多擴展運算符。官方是推薦引入immer,但這樣寫法又變復雜了一點。
另外zustand定義store時顆粒度需要挺細的,不然組件重復渲染的問題不好解決。不像MobX那樣可以把同一個頁面的store寫到一個文件里,zustand拆分的維度是需要按組件渲染狀態(tài)去劃分。
如果能像React toolkit那樣不需要用戶自己引入immer的話zustand還是挺香的。因為后臺系統(tǒng)一般來說交互類的狀態(tài)并不多,拆分顆粒度過細的問題并不大。而要開發(fā)人員自己每次都手動增加immer還是挺煩的。
總結(jié):需要引入額外的庫,store拆分要求比較細。
// 定義store
import produce from 'immer';
// list這個值不拆出去的話,在組件A修改title的值會引起list所在組件B的渲染。
const useStore = create<TitleState>((set) => ({
title: '',
list: [],
setTitle: (title) => set(produce((state: TitleState) => {
state.title = title;
})),
}));
// 組件A 使用title
const { title, setTitle } = useStore();
// 組件B 使用list
const { list } = useStore();
MobX
是的,說了一圈狀態(tài)管理庫最后還是選擇MobX。
MobX使用起來很簡單,主要用到useLocalStore跟useObserver兩個api。store可以按照頁面劃分,維護起來很方便。性能也好,按照store的值去拆分組件就行。
至于說React加MobX不如用vue的說法,可能從性能上說是這樣。但本質(zhì)上說選擇React主要是看重React衍生出來的其強大的生態(tài)環(huán)境,而不是其他原因。
舉一個典型例子就是React Native。如果有APP跨端開發(fā)需求的話,那么React Native還是比較熱門的解決方案。目前React從生態(tài)成熟度上來說有著其他框架都達不到的高度,前端團隊可以用React這一個框架去解決web、app、服務端渲染等多個場景的開發(fā)需求。使用一個技術棧能夠降低開發(fā)成本,開發(fā)人員切換開發(fā)場景的成本比較低,不需要學額外的框架語法。
所以說沒必要跟其他框架攀比,既然選擇了React,那就在React體系內(nèi)找一個好用的狀態(tài)管理庫就行,不要有其他的心理負擔。
// 一個文件定義一個頁面的store
class ListStore {
constructor() {
makeAutoObservable(this);
}
title = '';
list = [];
setList(values) {
this.list = values;
}
}
// 使用
const localStore = useLocalStore(() => store);
useObserver(() => (
<div>
{localStore.list.map((item) => (<div key={item.id}>{item.name}</div>))}
</div>
));
補充:關于React組件重復渲染問題,網(wǎng)上有些言論是覺得無所謂。
但如果不管的話當項目隨著時間而變得復雜之后很可能會遇到性能問題,到時候想改難度就變大了。
即使花大力氣重構之后也面臨測試問題,項目上線需要申請測試資源對業(yè)務功能進行回歸測試,總得來說還是比較麻煩的。
而MobX處理組件重復渲染問題挺方便的,只要組件拆分得當就不需要開發(fā)者過多關心。
三、hooks的使用問題與解決方案
技術棧選好之后接下來就是確定React代碼的開發(fā)形式了。
首先是目前在React項目中使用hooks的寫法是必須的,這是官方確定的路線。
問題但是用hooks會遇到兩個比較麻煩的問題,一個是useEffect、useCallback、useMemo這些API的依賴項過多時的問題。另一個是useEffect的使用問題。
依賴項問題:
先說依賴項問題,項目中遇到useEffect、useCallback、useMemo這些API最頭疼的是后面跟著好幾個依賴項,當你要去修改里面的功能時你必須查看每個依賴項的具體作用,了解它們的更新時期。新增加的狀態(tài)需要考慮是用useState還是用useRef,又或者是兩者并存??傊闹秦摀€是挺高的。
// 需要查看每個依賴項的更新邏輯
const onChange = useCallback(() => {
if (a) {
setValue(b);
}
}, [ a, b ]);
useEffect問題:
再來就是useEffect的使用問題,不管在項目里看到一個useEffect跟著多個依賴項還是多個useEffect跟著不同的依賴項,都是很頭疼的事情。
當你需要增加或者修改里面的代碼邏輯時你需要把代碼都理解一遍,然后再決定你新的代碼邏輯是寫在現(xiàn)有的useEffect里還是再新增一個useEffect去承接。
// 一個useEffect里有多個依賴項
useEffect(() => {}, [a, b, c])
// 多個useEffect跟著各自的依賴項
useEffect(() => {}, [a])
useEffect(() => {}, [b])
useEffect(() => {}, [c])
解決方案前面決定了mobx作為狀態(tài)管理庫,所以這兩個問題的解決方案就是盡量不要使用useState,服務端的接口請求使用ahooks去解決,剩下的交互狀態(tài)使用mobx處理。
依賴項多問題:
首先看依賴項過多的解決方案,當使用mobx的狀態(tài)之后依賴項只需要寫store一個依賴就行(不寫也行),這個時候在useEffect、useCallback這些API里面獲取的都是store里最新的值,不需要擔心狀態(tài)更新問題。
// 只需要寫localStore一個依賴,里面的a、b值永遠都是最新的
const onChange = useCallback(() => {
if (localStore.a) {
localStore.setValue(localStore.b);
}
}, [ localStore ]);// 也可以用[]
useEffect的使用問題:
然后是useEffect的使用問題,解決方案就是不使用useEffect。
跟上面依賴項多的解決方式一樣,服務端的接口請求都使用ahooks去解決,然后組件渲染狀態(tài)采用mobx結(jié)合ahooks提供的其他hooks方法(ahooks文檔),基本上就用不到useEffect了。
如果有監(jiān)聽某個值然后渲染層級嵌套比較深的組件的需求,比如父組件某個狀態(tài)變更之后需要孫子組件的form表單執(zhí)行清空動作的場景,那這個時候可以使用MobX的reaction去處理。
// 當狀態(tài)變更之后觸發(fā)
reaction(
() => localStore.visible,
visible => {
if (visible) {
formRef.current?.resetFields(); // 清空表單
}
}
);
補充:MobX組件復用問題可以參考官方文檔提供的寫法,通過傳入一個返回不同狀態(tài)值的函數(shù)去解決。
// 官方推薦寫法
const GenericNameDisplayer = observer(({ getName }) => <DisplayName name={getName()} />)
const MyComponent = ({ person, car }) => (
<>
<GenericNameDisplayer getName={() => person.name} />
<GenericNameDisplayer getName={() => car.model} />
<GenericNameDisplayer getName={() => car.manufacturer.name} />
</>
)
總結(jié)
1、系統(tǒng)的技術棧是React、React-router、antd、ahooks跟MobX。
2、狀態(tài)管理庫選擇MobX可以兼顧開發(fā)效率、后期維護跟性能問題。
3、hooks問題的解決方案主要是用ahooks處理服務端狀態(tài),然后用MobX處理剩下的交互狀態(tài);盡量少使用useState,不使用useEffect。
4、后續(xù)會補充一個代碼模板,把一些常用的后臺系統(tǒng)頁面的具體代碼組織形式補充進來。
5、最佳實踐的意義在于團隊內(nèi)部統(tǒng)一一個代碼寫法,以此實現(xiàn)降低項目開發(fā)成本以及同事之間協(xié)作成本的目標。因為代碼結(jié)構的一致可以方便項目后期的維護。假設說React官方推出了一個新的代碼組織形式,那么一個結(jié)構統(tǒng)一的項目就能夠快速遷移到新寫法上面(最理想情況是寫一個腳本批量把代碼進行替換)。而且團隊的開發(fā)人員也能快速理解不同項目的結(jié)構跟功能,不會出現(xiàn)某個項目只有某個同事能開發(fā)的情況。
6、本文這個最佳實踐是根據(jù)自身團隊情況設計的,如果也比較看中開發(fā)效率跟后期維護可以參考這個模式。
以上就是react后臺系統(tǒng)最佳實踐示例詳解的詳細內(nèi)容,更多關于react后臺系統(tǒng)實踐的資料請關注腳本之家其它相關文章!
相關文章
使用Axios在React中請求數(shù)據(jù)的方法詳解
這篇文章主要給大家介紹了初學React,如何規(guī)范的在react中請求數(shù)據(jù),主要介紹了使用axios進行簡單的數(shù)據(jù)獲取,加入狀態(tài)變量,優(yōu)化交互體驗,自定義hook進行數(shù)據(jù)獲取和使用useReducer改造請求,本文主要適合于剛接觸React的初學者以及不知道如何規(guī)范的在React中獲取數(shù)據(jù)的人2023-09-09
如何使用 electron-forge 搭建 React + Ts&n
本文介紹了如何使用Electron、electron-forge、webpack、TypeScript、React和SCSS等技術搭建一個桌面應用程序,通過這篇文章,開發(fā)者可以創(chuàng)建一個包含React組件、SCSS樣式、靜態(tài)資源和Loading頁面的應用,感興趣的朋友一起看看吧2025-01-01
antd中form表單的wrapperCol和labelCol問題詳解
最近學習中遇到了些問題,所以給大家總結(jié),下面這篇文章主要給大家介紹了關于antd中form表單的wrapperCol和labelCol問題的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2023-02-02
React實現(xiàn)頁面狀態(tài)緩存(keep-alive)的示例代碼
因為?react、vue都是單頁面應用,路由跳轉(zhuǎn)時,就會銷毀上一個頁面的組件,但是有些項目不想被銷毀,想保存狀態(tài),本文給大家介紹了React實現(xiàn)頁面狀態(tài)緩存(keep-alive)的代碼示例,需要的朋友可以參考下2024-01-01
React項目中報錯:Parsing error: The keyword &a
ESLint 默認使用的是 ES5 語法,如果你想使用 ES6 或者更新的語法,你需要在 ESLint 的配置文件如:.eslintrc.js等中設置 parserOptions,這篇文章主要介紹了React項目中報錯:Parsing error: The keyword 'import' is reservedeslint的問題及解決方法,需要的朋友可以參考下2023-12-12
解決React報錯`value` prop on `input` should&
這篇文章主要為大家介紹了React報錯`value` prop on `input` should not be null解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12
react的ui庫antd中form表單使用SelectTree反顯問題及解決
這篇文章主要介紹了react的ui庫antd中form表單使用SelectTree反顯問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01

