react后臺系統(tǒng)最佳實(shí)踐示例詳解
一、中后臺系統(tǒng)的技術(shù)棧選型
本文主要講三塊內(nèi)容:中后臺系統(tǒng)的技術(shù)棧選型、hooks時(shí)代狀態(tài)管理庫的選型以及hooks的使用問題與解決方案。
1. 要做什么
我們的目標(biāo)是搭建一個適用于公司內(nèi)部中后臺系統(tǒng)的前端項(xiàng)目最佳實(shí)踐。
2. 要求
由于業(yè)務(wù)需求比較多,一名開發(fā)人員需要負(fù)責(zé)幾個后臺系統(tǒng)。所以項(xiàng)目最佳實(shí)踐的要求按重要性排行:
1、開發(fā)效率。
2、可維護(hù)性。
3、性能。
總之,開發(fā)的高效率跟簡單的代碼結(jié)構(gòu)是比較側(cè)重的兩點(diǎn)。
3. 技術(shù)棧怎么選
由于我司前端技術(shù)棧主要使用React,所以基礎(chǔ)框架采用React跟React-router。項(xiàng)目開發(fā)內(nèi)容主要是做中后臺系統(tǒng)頁面,于是選擇antd作為系統(tǒng)的UI框架。然后ahooks提供了useAntdTable方法可以幫助我們節(jié)省二次封裝的工作量,所以采用ahooks作為項(xiàng)目主要使用的hooks庫。最后考慮到開發(fā)效率以及性能,狀態(tài)管理庫則采用MobX。
下面詳細(xì)說一下狀態(tài)管理庫的選型過程。
二、hooks時(shí)代狀態(tài)管理庫的選型
如果使用了ahooks或者React-Query這類帶數(shù)據(jù)請求方案的hooks庫,那已經(jīng)分擔(dān)了狀態(tài)管理庫很大一部分工作了。剩下的部分是頁面的交互狀態(tài)處理問題,主要是解決跨組件通信的問題。
目前調(diào)研的狀態(tài)管理方案有以下幾種:
context
首先考慮的是不引入任何狀態(tài)管理庫,直接使用React框架提供的context方法。
React context表面上使用起來很方便,只要定義一個provider并傳入數(shù)據(jù),使用的時(shí)候用useContext獲取對應(yīng)的值即可。
但這個方案需要開發(fā)者考慮如何處理組件重復(fù)渲染的問題,需要開發(fā)者考慮是通過手動拆分provider的數(shù)據(jù)還是使用memo、useMemo緩存組件的方案(詳情見這里)。
總的來說解決起來還是比較麻煩的,每次添加狀態(tài)都要檢查這個值是否要拆分、是否頻繁更新以及怎么組織組件比較合理等問題。
總結(jié):React context開發(fā)效率不高、后期維護(hù)麻煩。
// 需要拆分狀態(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這個方案首先要吐槽的是其繁瑣的寫法。每次使用的時(shí)候都要煩惱action怎么取名;使用reducer時(shí)要寫一大堆擴(kuò)展運(yùn)算符,而且一個請求至少要寫三個狀態(tài)(發(fā)送請求、請求成功、請求失敗);異步用thunk會被嫌棄不夠優(yōu)雅,而saga的API又多generator寫法又不好用。
官方的推出的Redux Toolkit框架解決了上面說的action的命名問題,還有reducer的要寫一堆擴(kuò)展運(yùn)算符的問題。但狀態(tài)顆粒度太細(xì)的問題還是存在,saga的寫法也還是沒變。
如果結(jié)合ahooks的話剛好是可以把saga節(jié)省掉,但用了這些請求庫之后redux鼓吹的狀態(tài)跟蹤的優(yōu)點(diǎn)也就消失了大半~~(雖然感覺這個功能也沒啥作用)~~。只是單純解決跨組件通信的話引入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) // 這一坨代碼實(shí)現(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ǎn)冗余。調(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,使用的時(shí)候直接調(diào)用就好。用起來比recoil方便多了。
但是呢zustand的狀態(tài)都是不可變,getState時(shí)跟redux一樣要用到很多擴(kuò)展運(yùn)算符。官方是推薦引入immer,但這樣寫法又變復(fù)雜了一點(diǎn)。
另外zustand定義store時(shí)顆粒度需要挺細(xì)的,不然組件重復(fù)渲染的問題不好解決。不像MobX那樣可以把同一個頁面的store寫到一個文件里,zustand拆分的維度是需要按組件渲染狀態(tài)去劃分。
如果能像React toolkit那樣不需要用戶自己引入immer的話zustand還是挺香的。因?yàn)楹笈_系統(tǒng)一般來說交互類的狀態(tài)并不多,拆分顆粒度過細(xì)的問題并不大。而要開發(fā)人員自己每次都手動增加immer還是挺煩的。
總結(jié):需要引入額外的庫,store拆分要求比較細(xì)。
// 定義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可以按照頁面劃分,維護(hù)起來很方便。性能也好,按照store的值去拆分組件就行。
至于說React加MobX不如用vue的說法,可能從性能上說是這樣。但本質(zhì)上說選擇React主要是看重React衍生出來的其強(qiáng)大的生態(tài)環(huán)境,而不是其他原因。
舉一個典型例子就是React Native。如果有APP跨端開發(fā)需求的話,那么React Native還是比較熱門的解決方案。目前React從生態(tài)成熟度上來說有著其他框架都達(dá)不到的高度,前端團(tuán)隊(duì)可以用React這一個框架去解決web、app、服務(wù)端渲染等多個場景的開發(fā)需求。使用一個技術(shù)棧能夠降低開發(fā)成本,開發(fā)人員切換開發(fā)場景的成本比較低,不需要學(xué)額外的框架語法。
所以說沒必要跟其他框架攀比,既然選擇了React,那就在React體系內(nèi)找一個好用的狀態(tài)管理庫就行,不要有其他的心理負(fù)擔(dān)。
// 一個文件定義一個頁面的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> ));
補(bǔ)充:關(guān)于React組件重復(fù)渲染問題,網(wǎng)上有些言論是覺得無所謂。
但如果不管的話當(dāng)項(xiàng)目隨著時(shí)間而變得復(fù)雜之后很可能會遇到性能問題,到時(shí)候想改難度就變大了。
即使花大力氣重構(gòu)之后也面臨測試問題,項(xiàng)目上線需要申請測試資源對業(yè)務(wù)功能進(jìn)行回歸測試,總得來說還是比較麻煩的。
而MobX處理組件重復(fù)渲染問題挺方便的,只要組件拆分得當(dāng)就不需要開發(fā)者過多關(guān)心。
三、hooks的使用問題與解決方案
技術(shù)棧選好之后接下來就是確定React代碼的開發(fā)形式了。
首先是目前在React項(xiàng)目中使用hooks的寫法是必須的,這是官方確定的路線。
問題但是用hooks會遇到兩個比較麻煩的問題,一個是useEffect、useCallback、useMemo這些API的依賴項(xiàng)過多時(shí)的問題。另一個是useEffect的使用問題。
依賴項(xiàng)問題:
先說依賴項(xiàng)問題,項(xiàng)目中遇到useEffect、useCallback、useMemo這些API最頭疼的是后面跟著好幾個依賴項(xiàng),當(dāng)你要去修改里面的功能時(shí)你必須查看每個依賴項(xiàng)的具體作用,了解它們的更新時(shí)期。新增加的狀態(tài)需要考慮是用useState還是用useRef,又或者是兩者并存。總之心智負(fù)擔(dān)還是挺高的。
// 需要查看每個依賴項(xiàng)的更新邏輯 const onChange = useCallback(() => { if (a) { setValue(b); } }, [ a, b ]);
useEffect問題:
再來就是useEffect的使用問題,不管在項(xiàng)目里看到一個useEffect跟著多個依賴項(xiàng)還是多個useEffect跟著不同的依賴項(xiàng),都是很頭疼的事情。
當(dāng)你需要增加或者修改里面的代碼邏輯時(shí)你需要把代碼都理解一遍,然后再決定你新的代碼邏輯是寫在現(xiàn)有的useEffect里還是再新增一個useEffect去承接。
// 一個useEffect里有多個依賴項(xiàng) useEffect(() => {}, [a, b, c]) // 多個useEffect跟著各自的依賴項(xiàng) useEffect(() => {}, [a]) useEffect(() => {}, [b]) useEffect(() => {}, [c])
解決方案前面決定了mobx作為狀態(tài)管理庫,所以這兩個問題的解決方案就是盡量不要使用useState,服務(wù)端的接口請求使用ahooks去解決,剩下的交互狀態(tài)使用mobx處理。
依賴項(xiàng)多問題:
首先看依賴項(xiàng)過多的解決方案,當(dāng)使用mobx的狀態(tài)之后依賴項(xiàng)只需要寫store一個依賴就行(不寫也行),這個時(shí)候在useEffect、useCallback這些API里面獲取的都是store里最新的值,不需要擔(dān)心狀態(tài)更新問題。
// 只需要寫localStore一個依賴,里面的a、b值永遠(yuǎn)都是最新的 const onChange = useCallback(() => { if (localStore.a) { localStore.setValue(localStore.b); } }, [ localStore ]);// 也可以用[]
useEffect的使用問題:
然后是useEffect的使用問題,解決方案就是不使用useEffect。
跟上面依賴項(xiàng)多的解決方式一樣,服務(wù)端的接口請求都使用ahooks去解決,然后組件渲染狀態(tài)采用mobx結(jié)合ahooks提供的其他hooks方法(ahooks文檔),基本上就用不到useEffect了。
如果有監(jiān)聽某個值然后渲染層級嵌套比較深的組件的需求,比如父組件某個狀態(tài)變更之后需要孫子組件的form表單執(zhí)行清空動作的場景,那這個時(shí)候可以使用MobX的reaction去處理。
// 當(dāng)狀態(tài)變更之后觸發(fā) reaction( () => localStore.visible, visible => { if (visible) { formRef.current?.resetFields(); // 清空表單 } } );
補(bǔ)充:MobX組件復(fù)用問題可以參考官方文檔提供的寫法,通過傳入一個返回不同狀態(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)的技術(shù)棧是React、React-router、antd、ahooks跟MobX。
2、狀態(tài)管理庫選擇MobX可以兼顧開發(fā)效率、后期維護(hù)跟性能問題。
3、hooks問題的解決方案主要是用ahooks處理服務(wù)端狀態(tài),然后用MobX處理剩下的交互狀態(tài);盡量少使用useState,不使用useEffect。
4、后續(xù)會補(bǔ)充一個代碼模板,把一些常用的后臺系統(tǒng)頁面的具體代碼組織形式補(bǔ)充進(jìn)來。
5、最佳實(shí)踐的意義在于團(tuán)隊(duì)內(nèi)部統(tǒng)一一個代碼寫法,以此實(shí)現(xiàn)降低項(xiàng)目開發(fā)成本以及同事之間協(xié)作成本的目標(biāo)。因?yàn)榇a結(jié)構(gòu)的一致可以方便項(xiàng)目后期的維護(hù)。假設(shè)說React官方推出了一個新的代碼組織形式,那么一個結(jié)構(gòu)統(tǒng)一的項(xiàng)目就能夠快速遷移到新寫法上面(最理想情況是寫一個腳本批量把代碼進(jìn)行替換)。而且團(tuán)隊(duì)的開發(fā)人員也能快速理解不同項(xiàng)目的結(jié)構(gòu)跟功能,不會出現(xiàn)某個項(xiàng)目只有某個同事能開發(fā)的情況。
6、本文這個最佳實(shí)踐是根據(jù)自身團(tuán)隊(duì)情況設(shè)計(jì)的,如果也比較看中開發(fā)效率跟后期維護(hù)可以參考這個模式。
以上就是react后臺系統(tǒng)最佳實(shí)踐示例詳解的詳細(xì)內(nèi)容,更多關(guān)于react后臺系統(tǒng)實(shí)踐的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
前端 react 實(shí)現(xiàn)圖片上傳前壓縮(縮率圖)
這篇文章主要介紹了前端 react 實(shí)現(xiàn)圖片上傳前壓縮(縮率圖),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-08-08簡談創(chuàng)建React Component的幾種方式
這篇文章主要介紹了創(chuàng)建React Component的幾種方式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,,需要的朋友可以參考下2019-06-06React Hook - 自定義Hook的基本使用和案例講解
自定義Hook本質(zhì)上只是一種函數(shù)代碼邏輯的抽取,嚴(yán)格意義上來說,它本身并不算React的特性,這篇文章主要介紹了React類組件和函數(shù)組件對比-Hooks的介紹及初體驗(yàn),需要的朋友可以參考下2022-11-11React-Native做一個文本輸入框組件的實(shí)現(xiàn)代碼
這篇文章主要介紹了React-Native做一個文本輸入框組件的實(shí)現(xiàn)代碼,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-08-08