Redux store 的動態注入
前言
在 React + Redux + React-Router 的單頁應用架構中,我們將 UI 層( React 組件)和數據層( Redux store )分離開來,以做到更好地管理應用的。
Redux store 既是存儲整個應用的數據狀態,它的 state 是一個樹的數據結構,可以看到如圖的例子:
而隨著應用和業務邏輯的增大,項目中的業務組件和數據狀態也會越來越多;在 Router 層面可以使用 React-Router 結合 webpack 按需載入 以減少單個 js 包的大小。
而在 store 層面,隨著應用增大,整個結構可能會變的非常的大,應用載入初始化的時候就會去初始化定義整個應用的 store state 和 actions ,這對與內存和資源的大小都是一個比較大的佔用和消耗。
因此如何做到像 Router 一樣地在需要某一塊業務組件的時候再去添加這部分的 Redux 相關的數據呢?
Redux store 動態注入 的方案則是用以解決以上的問題。
在閱讀本文的時候建議了解以下一些概念:
- - (Redux 的數據流概念與文檔)
- - (React-Router v4 文檔)
- - (redux 大法好 —— 入門實例 TodoList)
方案實踐
原理
在 Redux 中,對於 store state 的定義是通過組合 reducer 函數來得到的,也就是說 reducer 決定了最後的整個狀態的數據結構。在生成的 store 中有一個 replaceReducer(nextReducer) 方法,它是 Redux 中的一個高階 API ,該函數接收一個 `nextReducer` 參數,用於替換 store 中原原有的 reducer ,以此可以改變 store 中原有的狀態的數據結構。
因此,在初始化 store 的時候,我們可以只定義一些默認公用 reducer(登錄狀態、全局信息等等),也就是在 `createStore` 函數中只傳入這部分相關的 reducer ,這時候其狀態的數據結構如下:
當我們載入到某一個業務邏輯對應的頁面時,比如 `/home`,這部分的業務代碼經過 Router 中的處理是按需載入的,在其初始化該部分的組件之前,我們可以在 store 中注入該模塊對應的 reducer ,這時候其整體狀態的數據結構應該如下:
在這裡需要做的就是將新增的 reducer 與原有的 reducer 組合,然後通過 `store.replaceReducer` 函數更新其 reducer 來做到在 store 中的動態注入。
代碼
話不多說,直接上代碼 TongchengQiu/react-redux-dynamic-injection ,示例項目的目錄結構如下:
.├── src| ├── pages| | ├── Detail| | | ├── index.js| | | ├── index.jsx| | | └── reducer.jsx| | ├── Home| | | ├── index.js| | | ├── index.jsx| | | └── reducer.jsx| | ├── List| | | ├── index.js| | | ├── index.jsx| | | └── reducer.jsx| | ├── Root.js| | └── rootReducer.js| ├── store| | ├── createStore.js| | ├── location.js| | └── reducerUtil.js| └── index.js└── package.json
入口
首先來看整個應用的入口文件 `./src/index.js` :
import React from "react";import ReactDOM from "react-dom";import Root from "./pages/Root";ReactDOM.render(<Root />, document.getElementById("root"));
這裡所做的就是在 `#root` DOM 元素上掛載渲染 `Root` 組件;
Root 根組件
在 `./src/pages/Root.jsx` 中:
import React, { Component} from "react";import { Provider } from "react-redux";import { Link, Switch, Route, Router as BrowserRouter } from "react-router";import createStore from "../store/createStore";import { injectReducer } from "../store/reducerUtils";import reducer, { key } from "./rootReducer";export const store = createStore({} , { [key]: reducer});const lazyLoader = (importComponent) => ( class AsyncComponent extends Component { state = { C: null } async componentDidMount () { const { default: C } = await importComponent(); this.setState({ C }); } render () { const { C } = this.state; return C ? <C {...this.props} /> : null; } });export default class Root extends Component { render () { return ( <div className="root__container"> <Provider store={store}> <Router> <div className="root__content"> <Link to="/">Home</Link> <br /> <Link to="/list">List</Link> <br /> <Link to="/detail">Detail</Link> <Switch> <Route exact path="/" component={lazyLoader(() => import("./Home"))} /> <Route path="/list" component={lazyLoader(() => import("./List"))} /> <Route path="/detail" component={lazyLoader(() => import("./Detail"))} /> </Switch> </div> </Router> </Provider> </div> ); }}
首先是創建了一個 Redux 的 store ,這裡的 `createStore` 函數並並沒有用 Redux 中原生提供的,而是重新封裝了一層來改造它;
它接收兩個參數,第一個是初始化的狀態數據,第二個是初始化的 reducer,這裡傳入的是一個名稱為 `key` 的 reducer ,這裡的 `key` 和 `reducer` 是在 `./src/pages/rootReducer.js` 中定義的,它用來存儲一些通用和全局的狀態數據和處理函數的;
`lazyLoader` 函數是用來非同步載入組件的,也就是通過不同的 route 來分割代碼做按需載入,具體可參考 (code-splitting) ;
他的用法就是在 `Route` 組件中傳入的 `component` 使用 `lazyLoader(() => import("./List"))` 的方式來導入;
接下來就是定義了一個 `Root` 組件並暴露,其中 `Provider` 是用來連接 Redux store 和 React 組件,這裡需要傳入 `store` 對象。
創建 STORE
前面提到,創建 store 的函數是重新封裝 Redux 提供的 `createStore` 函數,那麼這裡面做了什麼處理的?
看 `./src/store/createStore.js` 文件:
import { applyMiddleware, compose, createStore } from "redux";import thunk from "redux-thunk";import { makeAllReducer } from "./reducerUtils";export default (initialState = {}, initialReducer = {}) => { const middlewares = [thunk]; const enhancers = []; if (process.env.NODE_ENV === "development") { const devToolsExtension = window.devToolsExtension; if (typeof devToolsExtension === "function") { enhancers.push(devToolsExtension()); } } const store = createStore( makeAllReducer(initialReducer), initialState, compose( applyMiddleware(...middlewares), ...enhancers ) ); store.asyncReducers = { ...initialReducer }; return store;}
首先在暴露出的 `createStore` 函數中,先是定義了 Redux 中我們需要的一些 `middlewares` 和 `enhancers` :
- - [`redux-thunk`](gaearon/redux-thunk) 是用來在 Redux 中更好的處理非同步操作的;
- - `devToolsExtension` 是在開發環境下可以在 chrome 的 redux devtool 中觀察數據變化;
之後就是生成了 store ,其中傳入的 reducer 是由 `makeAllReducer` 函數生成的;
最後返回 store ,在這之前給 `store` 增加了一個 `asyncReducers` 的屬性對象,它的作用就是用來緩存舊的 reducers 然後與新的 reducer 合併,其具體的操作是在 `injectReducer` 中;
生成 REDUCER
在 `./src/store/reducerUtils.js` 中:
import { combineReducers } from "redux";export const makeAllReducer = (asyncReducers) => combineReducers({ ...asyncReducers});export const injectReducer = (store, { key, reducer }) => { if (Object.hasOwnProperty.call(store.asyncReducers, key)) return; store.asyncReducers[key] = reducer; store.replaceReducer(makeAllReducer(store.asyncReducers));}export const createReducer = (initialState, ACTION_HANDLES) => ( (state = initialState, action) => { const handler = ACTION_HANDLES[action.type]; return handler ? handler(state, action) : state; });
在初始化創建 store 的時候,其中的 reducer 是由 `makeAllReducer` 函數來生成的,這裡接收一個 `asyncReducers` 參數,它是一個包含 `key` 和 `reducer` 函數的對象;
`injectReducer` 函數是用來在 store 中動態注入 reducer 的,首先判斷當前 store 中的 `asyncReducers` 是否存在該 reducer ,如果存在則不需要做處理,而這裡的 `asyncReducers` 則是存儲當前已有的 reducers ;
如果需要新增 reducer ,則在 `asyncReducers` 對象中加入新增的 reducer ,然後通過 `makeAllReducer` 函數返回原有的 reducer 和新的 reducer 的合併,並通過 `store.replaceReducer` 函數替換 `store` 中的 reducer。
`createReducer` 函數則是用來生成一個新的 reducer 。
定義 ACTION 與 REDUCER
關於如何定義一個 action 與 reducer 這裡以 rootReducer 的定義來示例 `./src/pages/rootReducer.js` :
import { createReducer } from "../store/reducerUtils";export const key = "root";export const ROOT_AUTH = `${key}/ROOT_AUTH`;export const auth = () => ( (dispatch, getState) => ( new Promise((resolve) => { setTimeout(() => { dispatch({ type: ROOT_AUTH, payload: true }); resolve(); }, 300); }) ));export const actions = { auth};const ACTION_HANLDERS = { [ROOT_AUTH]: (state, action) => ({ ...state, auth: action.payload })};const initalState = { auth: false};export default createReducer(initalState, ACTION_HANLDERS);
這一步其實比較簡單,主要是結合 `redux-thunk` 的非同步操作做了一個模擬 auth 驗證的函數;
首先是定義了這個 reducer 對應的 state 在根節點中的 key ;
然後定義了 actions ;
之後定義了操作函數 auth ,其實就是觸發一個 `ROOT_AUTH` 的 action;
之後定義 actions 對應的處理函數,存儲在 `ACTION_HANLDERS` 對象中;
最後通過 `createReducer` 函數生成一個 reducer 並暴露出去;
對於在業務組件中需要動態注入的 reducer 的定義也是按照這套模式,具體可以觀察每個業務組件中的 `reducer.js` 文件;
動態注入 REDUCER
在前面,我們生成了一個 store 並賦予其初始化的 state 和 reducer ,當我們載入到某一塊業務組件的時候,則需要動態注入該組件對應的一些 state 和 reducer。
以 Home 組件為示例,當載入到該組件的時候,首先執行 `index.js` 文件:
import { injectReducer } from "../../store/reducerUtils";import { store } from "../Root";import Home from "./index.jsx";import reducer, { key } from "./reducer";injectReducer(store, { key, reducer });export default Home;
首先是在 store 中插入其業務模塊對於的 reducer: `injectReducer(store, { key, reducer })` ,之後直接暴露該組件;
因此在該組件初始化之前,在 store 中就注入了其對應的 state 和 reducer;
而在 `index.jsx` 中對於 Redux 的使用和其標準的用法並無區別;感興趣可以閱讀該部分的代碼。
運行示例
clone 倉庫:
git clone TongchengQiu/react-redux-dynamic-injection
初始化:
npm i -d
運行:
npm start
可以看到啟動了項目 http://localhost:3000/;
通過 Redux Devtool ,可以看到這裡的初始狀態為:
點擊 List 到 List 對應的頁面,可以看到原來的狀態變為了:
也就是說在載入到 List 組件的時候,動態插入了這部分對應的 state 和 reducer。
推薦閱讀: