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