Redux + Hooks 工程實(shí)踐
點(diǎn)擊上方關(guān)注 前端技術(shù)江湖,一起學(xué)習(xí),天天進(jìn)步
“都 1202 年了怎么還有人在用 Redux”——這大概不少人看到這篇文章的第一反應(yīng)。首先先表明一下,這篇文章并不討論是不是應(yīng)該使用 Redux,這是一個(gè)比較大的話題,應(yīng)該單獨(dú)水一篇。而且社區(qū)已經(jīng)存在許許多多的討論了,你總能從幾篇高贊的文章中找到一些優(yōu)缺點(diǎn)的對(duì)比圖,然后結(jié)合你項(xiàng)目的場(chǎng)景最終作出決定。我們來(lái)隨便舉幾個(gè)團(tuán)隊(duì)使用 Redux 的原因。首先是易懂,Redux 被人吐槽很多的可能是寫(xiě)法繁瑣,但是在繁瑣寫(xiě)法的背后就沒(méi)有那么多黑科技了,非常容易排查問(wèn)題。另外,Redux 本質(zhì)是對(duì)邏輯處理方式提出了標(biāo)準(zhǔn)范式,并且搭配得給到了一組實(shí)踐規(guī)范,有助于保持項(xiàng)目代碼書(shū)寫(xiě)風(fēng)格與組織方式的一致性,這點(diǎn)在多人合作開(kāi)發(fā)的項(xiàng)目里面尤為重要。其他的優(yōu)點(diǎn)就不在此贅述啦。
這時(shí)候就有同學(xué)可能要問(wèn)了,你講 Redux,那和 hooks 又有啥子關(guān)系呢。眾所周知,在 React 團(tuán)隊(duì)推出 Hooks 這個(gè)概念后不久,Redux 也更新了對(duì)應(yīng)的 API 來(lái)支持。Hooks 的本質(zhì)是對(duì)邏輯的封裝以及邏輯與 UI 代碼的解耦。有了 Hooks 的加持能夠讓我們的 Redux React 項(xiàng)目更加簡(jiǎn)潔、易懂、擴(kuò)展性更強(qiáng)。而且 Hooks API 在 Redux 的最佳實(shí)踐建議中目前是 Level 2 的強(qiáng)烈推薦使用級(jí)別。他擁有更簡(jiǎn)潔的表達(dá)方式,更干凈的 React 節(jié)點(diǎn)數(shù),更友好的 typescript 支持。
具體 Redux 相關(guān)的 API 怎么用,這里不做介紹,可以直接跳轉(zhuǎn)官方文檔進(jìn)行了解。下面我們會(huì)從一個(gè)應(yīng)用場(chǎng)景來(lái)具體講一講,他們是怎么幫助我們更好地組織代碼的。其中的部分工程級(jí)別代碼來(lái)自于 react-boilerplate 的項(xiàng)目模版,它在動(dòng)態(tài)加載問(wèn)題上提供了不少幫助。
封裝案例
在開(kāi)發(fā)大型 React 應(yīng)用的時(shí)候,動(dòng)態(tài)懶加載代碼永遠(yuǎn)是我們項(xiàng)目架構(gòu)中的必選項(xiàng)。代碼的拆分、動(dòng)態(tài)引用等,工程化工具都已經(jīng)幫我們完成了。我們更需要關(guān)注的是,動(dòng)態(tài)引入與解除掛載等操作時(shí)額外要做什么,以及這個(gè)工作如何盡量少的暴露給項(xiàng)目開(kāi)發(fā)者。前面說(shuō)過(guò)了,Hooks 最強(qiáng)大的能力在于邏輯的封裝,這里當(dāng)然也就要借助他的力量了。
這里我們以 Reducer 作為例子來(lái)講,其他中間件,例如 Saga 等都可以類(lèi)推,如果需要可以后續(xù)再把相應(yīng)的代碼一并貼出來(lái)。我們把整個(gè)封裝分為三層:核心實(shí)現(xiàn)、可組合封裝、對(duì)開(kāi)發(fā)者暴露封裝。下面我們按順序一一講解。(具體實(shí)現(xiàn)中我都會(huì)默認(rèn)帶上包含 connected router 的實(shí)現(xiàn),方便需要抄代碼的可以直接用)
核心實(shí)現(xiàn)
這里的代碼實(shí)現(xiàn)的是如何為一個(gè) store 掛載與解除掛載拆分后的各個(gè) Reducer 的邏輯。
// 本段代碼完全來(lái)自于 react-boilerplate 項(xiàng)目
import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router';
import invariant from 'invariant';
import { isEmpty, isFunction, isString } from 'lodash';
import history from '@/utils/history';
import checkStore from './checkStore'; // 做類(lèi)型安全檢測(cè)的,不用關(guān)心
function createReducer(injectedReducers = {}) {
return history => combineReducers({
router: connectRouter(history),
...injectedReducers,
});
}
export function injectReducerFactory(store, isValid) {
return function injectReducer(key, reducer) {
if (!isValid) checkStore(store);
invariant(
isString(key) && !isEmpty(key) && isFunction(reducer),
'(src/utils...) injectReducer: Expected `reducer` to be a reducer function',
);
if (
Reflect.has(store.injectedReducers, key)
&& store.injectedReducers[key] === reducer
) return;
store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
store.replaceReducer(createReducer(store.injectedReducers)(history));
};
}
export default function getInjectors(store) {
checkStore(store);
return {
injectReducer: injectReducerFactory(store, true),
};
}
這段有個(gè)點(diǎn)比較特殊,需要講一下。你可能會(huì)發(fā)現(xiàn),這里面根本沒(méi)有解除掛載的部分。這是因?yàn)?nbsp;reducer 比較特殊,他并不會(huì)產(chǎn)生副作用,并且因?yàn)槟壳疤峁┑姆椒ㄊ峭ㄟ^(guò)整個(gè)替換的方式去掛載新的 Reducer,所以并沒(méi)有什么必要去單獨(dú)做解除掛載。在處理其他中間件的掛載時(shí),特別是那些存在副作用的(例如 redux-saga),我們需要對(duì)應(yīng)地實(shí)現(xiàn)一個(gè)解除掛載的 eject 方法。
OK,那么現(xiàn)在我們已經(jīng)可以通過(guò) getInjectors 方法為整個(gè)項(xiàng)目提供一個(gè) injectReducer 注入 Reducer 的能力了(同時(shí)可能包含 eject 方法)。下一步就是怎么調(diào)度這個(gè)能力。
可組合的封裝
這里,我們希望通過(guò)一個(gè)自定義的 hooks,可以允許開(kāi)發(fā)者為一個(gè)組件聲明某一個(gè) 命名空間 的 reducer 與其生命周期一致地進(jìn)行掛載與解除掛載。開(kāi)發(fā)者只需要傳入 reducer 的命名空間與 reducer 實(shí)現(xiàn),并將這個(gè) hooks 放到相應(yīng)的組件邏輯中即可。
import React from 'react';
import { ReactReduxContext } from 'react-redux';
// 這是我們?cè)谏弦徊綄?shí)現(xiàn)的 injector 工廠,通過(guò)他來(lái)產(chǎn)出一個(gè)與固定 store 綁定的 injectReducer 函數(shù)
import getInjectors from './reducerInjectors';
const useInjectReducer = ({ key, reducer }) => {
// 需要從 Redux 的 context 中獲取到當(dāng)前應(yīng)用的全局 store 實(shí)例
const context = React.useContext(ReactReduxContext);
// 為了模擬 constructor 的運(yùn)行時(shí)機(jī)
const initFlagRef = React.useRef(false);
if (!initFlagRef.current) {
initFlagRef.current = true;
getInjectors(context.store).injectReducer(key, reducer);
}
// 如果需要加入 eject 的邏輯,則可以使用這樣的寫(xiě)法。類(lèi)似于為當(dāng)前組件增加一個(gè) willUnmount 的生命周期邏輯。
// React.useEffect(() => (() => {
// const injectors = getInjectors(context.store);
// injectors.ejectReducer(key);
// }), []);
};
export { useInjectReducer };
useInjectReducer 這個(gè) Hooks 幫助我們處理了何時(shí)去掛載,怎么掛載等問(wèn)題,我們最終只需要告訴他 掛載什么 就可以了。通過(guò)這層封裝,可以發(fā)現(xiàn)我們進(jìn)一步收斂了關(guān)注點(diǎn)。到這一步為止,我們都是提供了一個(gè)項(xiàng)目級(jí)別的公共方法。在下一步中,我們會(huì)提供一個(gè)統(tǒng)一的寫(xiě)法,在具體的開(kāi)發(fā)過(guò)程中去使用,進(jìn)一步做封裝收斂。
在進(jìn)入下一步之前,我們先簡(jiǎn)單解釋一下上面的邏輯。邏輯通過(guò)注釋分為了三段(第三段在 reducer 場(chǎng)景下沒(méi)用到),第一段我們通過(guò)當(dāng)前組件所處的 redux 上下文,拿到了 store 的引用,第二段與第三段我們分別讓組件在 初始化 和 銷(xiāo)毀前 執(zhí)行掛載與解除掛載的操作。通過(guò)一個(gè) initFlagRef 為 functional 的組件模擬構(gòu)造器的生命周期(如果有更好的實(shí)現(xiàn)方案歡迎指教),因?yàn)槿绻趻燧d之后再 inject 的話,會(huì)在第一次渲染時(shí)取不到對(duì)應(yīng) store 的內(nèi)容。
對(duì)開(kāi)發(fā)者暴露封裝
在完成公用方法的封裝之后,我們下一步考慮的就是如何用更簡(jiǎn)單的方式,為我們的模塊掛載 store 。按照下面的方式,開(kāi)發(fā)者不用關(guān)心任何東西,只需一句話就可以完成掛載,也不用提供額外的參數(shù)。如果同時(shí)有 reducer、saga 或其他中間件內(nèi)容,也可以一起打包搞定。
import {
useInjectReducer,
// useInjectSaga,
} from '@/utils/store';
import actions from './actions';
import constants from './constants';
import reducer from './reducer';
// import saga from './saga';
const namespace = constants.namespace;
const useSubStore = () => {
useInjectReducer({ key: namespace, reducer });
// useInjectSaga({ key: namespace, saga });
};
export {
namespace,
actions,
constants,
useSubStore,
};
實(shí)際使用范例:
import React from 'react';
import {
useSubStore,
} from './store';
export default function Page() {
useSubStore();
return <div />;
};
具體的數(shù)據(jù)和邏輯我們也可以封裝成幾個(gè) Hooks ,例如我們需要提供一個(gè)數(shù)組數(shù)據(jù)簡(jiǎn)單操作,我們只關(guān)心 添加 和 數(shù)量,就可以封裝一個(gè) Hooks,這樣實(shí)際使用方只需要關(guān)心 添加 和 數(shù)量 這兩個(gè)要素,不用關(guān)心 redux 的具體實(shí)現(xiàn)方式了。
import { useMemo, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
actions, constants, namespace,
} from './store';
export function useItemList() {
const dispatch = useDispatch();
const list = useSelector(state => state[namespace].itemList);
// 這只是范例!
const count = useMemo(() => list.length, [list]);
const add = useCallback((item) => dispatch(actions.addItem(item)), []);
return [count, add];
}
下面我們修改一下使用的地方:
import React from 'react';
import {
useSubStore,
} from './store';
import { useItemList } from './useItemList';
export default function Page() {
useSubStore();
const [count, add] = useItemList();
return <div onClick={() => add({})}>{count}</div>;
};
通過(guò)這樣一種拆分方式,store 的定義,store 的使用邏輯,業(yè)務(wù)側(cè)三者都只關(guān)注自己必須關(guān)注的部分,任何一方改動(dòng)都可以盡量少地引起變更。
可復(fù)用的 Hooks
那我們進(jìn)一步思考一下,以前我們可能一個(gè)頁(yè)面對(duì)應(yīng)一個(gè) store。通過(guò) Hooks 進(jìn)行拆分后,我們更方便從功能層面去拆分 store,store 的邏輯也會(huì)更為清晰。與 store 的交互被封裝成了 Hooks 之后也可以很快在多個(gè)展示層被使用。這在復(fù)雜 B 端工作臺(tái)場(chǎng)景下會(huì)展現(xiàn)出很大的價(jià)值。案例會(huì)有點(diǎn)長(zhǎng),以后有時(shí)間可以再補(bǔ)上。
回顧
看完上面的例子,相信聰明的讀者已經(jīng)知道我想表達(dá)的問(wèn)題了。通過(guò)結(jié)合 Redux + Hooks,標(biāo)準(zhǔn)化了定義代碼,對(duì)邏輯、調(diào)用、定義三者一定程度上進(jìn)行了解耦。通過(guò)簡(jiǎn)化的 API,減少了邏輯的理解成本,減少了后續(xù)維護(hù)的復(fù)雜度,一定程度上還可以達(dá)到復(fù)用。不管是相較于過(guò)去的 Redux 接入方案,還是相較于單純使用 Hooks,都有著其獨(dú)特的優(yōu)勢(shì)。特別適用于邏輯相對(duì)復(fù)雜的工作臺(tái)場(chǎng)景。(而且我很喜歡 Saga的設(shè)計(jì)思路,能用起來(lái)就很爽)。
OK,收。這次以一個(gè)簡(jiǎn)單的例子,稍稍展示了一下在 Hooks 大環(huán)境下 Redux 與其產(chǎn)生的化學(xué)反應(yīng)。主要想展示的是依賴(lài) Hooks 的邏輯可封裝能力的一種設(shè)計(jì)思路,Redux 黑的同學(xué)們不要過(guò)多糾結(jié)與這個(gè)選型,蘿卜青菜各有所愛(ài)。
希望這個(gè)系列能繼續(xù)寫(xiě)下去 :)
作者:ES2049 / armslave00 https://zhuanlan.zhihu.com/p/374788504 非常歡迎有激情的你加入 ES2049 Studio,簡(jiǎn)歷請(qǐng)發(fā)送至 [email protected] 。
The End
歡迎自薦投稿到《前端技術(shù)江湖》,如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),記得點(diǎn)個(gè) 「在看」哦
點(diǎn)個(gè)『在看』支持下 
