Redux 在實踐中的一些問題及思考
React 絕對是 2015 年前端領域的關鍵詞,基于 React 的 Flux 架構也被越來越多的人所熟識。然而 Flux 作為一套架構思想而不是框架讓許多開發者在實踐中摸不著頭腦,因此社區里也誕生了很多基于 Flux 的「輪子」。而今天要說的就是其中最火、逼格最高的輪子 —— Redux。
本文不是一篇介紹 Redux 的入門文章(如果大家有興趣的話,后面可以寫寫),因此閱讀本文至少需要以下幾點基礎:
既然不是基礎,那么本文著重要講的是什么呢?其實是我在實際業務中使用 Redux 遇到的一些問題及我自己的思考。促使我寫這篇文章的另一個原因是,知乎上有一位朋友私信咨詢了我一些 Redux 相關的問題,讓我了解到原來我遇到的問題大家也會有疑惑,因此總結成文,方便后人參考。
本文將會用類似 cookbook 的形式,通過一問一答來嘗試解釋 Redux 在實際應用中的問題。
問題一:一個 action 被 reducer1 處理完之后,希望 reducer2 也對這個 action 做出響應,該怎么處理?
這個問題其實要從兩個方向考慮:即 reducer1 和 reducer2 對某個 action 的響應是否有先后順序的限制。
若沒有,則在 reducer1 和 reducer2 的 swtich..case.. 中針對同一個 ACTION_TYPE 做出處理即可。至于 ACTION_TYPE ,則可以在觸發這個 action 的 actionCreator.js 里 export 出來。如
// actionCreator.js export const SOME_TYPE = 'SOME_ACTION_TYPE'; export function doSomeAction() { return { type: SOME_TYPE, payload: 123 }; } // reducer1.js import { SOME_TYPE } from './actionCreator'; export default function reducer(state, action) { switch(action.type) { case SOME_TYPE: { // 這這個 action 做出響應 } ... } // reducer2.js 同 reducer1.js
若有先后順序限制,則比較復雜。我們站在整個應用的角度考慮,其實我們的需求是當觸發一個 action 時,希望 reducer1 先響應這個 action 更新 state,然后 reducer2 再響應這個 action,也許還需要基于 reducer1 中最新的 store 來更新自己的 state。
這個時候,可以用我自己寫的一個 Redux 中間件 —— redux-combine-action 。使用這種中間件后在 actionCreator 中的邏輯大概是這樣的:
// actionCreator.js export function doSomeAction() { return [ (getState, dispatch) => { dispatch({ type: ACTION_TYPE_1, payload: 123 }); }, (getState, dispatch) => { const state = getState(); dispatch({ type: ACTION_TYPE_2, payload: state.some.useful.data.from.state }); } ]; }
可以看到我們在 actionCreator 中返回了一個函數數組,每個函數都獲得了 getState 和 dispatch 方法。在上述代碼中,首先會 dispatch 一個 ACTION_TYPE_1 的 action,假設會被 reducer1 處理,然后會 dispatch 一個 ACTION_TYPE_2 類型的 action,則會被 reducer2 處理。
對于 reducer 來說,并不關系你的業務邏輯是先處理哪一個后處理哪一個,它們只是單純的響應每一個 action 而已。
至于其中的原理,可以自行查看源代碼。(寫到這的時候,自覺滾去把這個庫的測試補上了)需要說明的是,這個中間件的核心思想基本是參考自 redux-combine-actions 庫。
問題二:Redux 中如何實現一個公共業務組件?
一個所謂的「公共業務組件」,應該包含了 UI 和數據兩部分。比如淘寶上的收貨地址選擇控件,應該就包含了三個 select 選擇器和對應的省市區數據。這樣一個地址選擇控件的邏輯在各個模塊中都是一致的,但是怎么設計才能實現復用呢?
一個關于 Flux/Redux 的基礎概念:任何一個 actionCreator 觸發的 action 都會通知到所有的 store/reducer。
考慮一種極端情況,頁面上同時有兩個模塊都使用了這個公共的地址選擇組件。當在這個組件里選擇了一個省份時,會 dispatch 一個 SELECT_PROVINCE 的 action,那么兩個模塊怎么知道用戶到底是選擇了哪個模塊里公共組件的省份呢?
再考慮另外一個問題,當用戶選擇了一個省份后,某一個業務模塊希望能夠再做一些別的數據處理(比如根據當前選擇的省份加載對應的熱銷寶貝)該怎么操作呢?總不能在 reducer 中 dispatch 一個 action 吧?( 注意,這是 anti-pattern,不要在 reducer 中 dispatch action!! )
其實這兩個問題的解決思路是類似的,就是在公共組件中,數據傳遞的過程中加上更多的限制,我能想到的有這么兩種解決方案:
- 公共組件自己維護狀態,業務模塊通過給公共組件傳入回調來獲得公共組件的數據變動,再 dispatch 響應的 action 更新自己的 state。這樣設計的問題在于,違背了 Redux 倡導的無狀態原則,公共組件內部存在了 state 和 setState 操作。
- 通過 props 傳入指定的 actionType 或 actionCreator(傳入 actionCreator 是我同事奇陽的想法)來讓公共組件 dispatch 不同的 action,以此達到區分的效果。
一個簡單的例子:
// 公共模塊自己定義的 actionCreator import { provinceActionCreator } from './actionCreator'; // 一個公共的地址選擇組件 const AddressPicker = React.createClass({ handleChangeProvince(e) { // 優先使用 props 中傳入的 actionCreator const provinceActionCreator = this.props.provinceActionCreator || provinceActionCreator; // dispatch 一個選擇省份的 action this.props.dispatch(provinceActionCreator(e.target.value)); }, render() { return ( <select onChange={this.handleChangeProvince}> <option>北京</option> <option>浙江</option> </select> ); } });
問題三:Redux 中跟路由有關的狀態應該怎么維護?
現在用 React 做路由的話大家基本都會選擇 react-router(這個庫我已經吐槽過無數次了,經常改 API),而用 Redux 的話自然也是選擇基于 react-router 封裝的 redux-router。
在講詳細的問題之前,先要闡明一個觀點,即「路由也是應用狀態的一部分」,應該也的確有一個單獨的 store 在維護它。
既然有一個單獨的 store,那么其中的狀態就能輕松的獲取和設置了,具體的操作方法可以參考 redux-router 中的 API。
如果拋開 redux-router 不談,react-router 中針對路由切換時的數據傳遞提供了一種全新的思路,即在切換路由時,將「state」存入 session-storage 中,借用這個功能你就可以輕松的實現「回退」操作了。
詳細說明見 history 模塊的文檔 。
小結
總的來說,使用 Redux 開發業務功能還是很爽的,尤其是對于數據流動復雜的應用,單一的數據流動、類 Immutable 的數據變動方式以及酷炫的 devtool 這些特性簡直堪稱神器。