redux 原生寫法實現功能須要寫大量的 action、reducer 模板代碼,對於複雜項目來講模板代碼太多,且 action 文件和 reducer 文件等不停切換,開發體驗較差。你們通常都會封裝簡化。react
好比 dva 的實現方式,引入了 effects,把 state、effects、action、reducer 放在一個 model 文件,簡化了 action 和 reducer 的定義方式。相似的還有 rematch,mirror。git
今年項目重構時,因路由緩存等緣由,沒有采用現有 dva 等框架,簡易封裝實現了相似 dva 的 redux 數據流實現。github
簡單封裝實現下相似 api 和使用方式。redux
直接上代碼,實現 redux 主體功能封裝api
import { createStore } from 'redux';
import { Provider } from 'react-redux';
// 爲 effects 或 reducer 添加 namespace, 方便保存到全局
const addNamespace = (obj, name) => {
const newObj = {};
Object.keys(obj).forEach(item => {
newObj[`${name}/${item}`] = obj[item];
});
return newObj;
};
class Zoo {
constructor() {
// 定義公共 state、store、effects 等
this.state = {};
this.models = {};
this.reducers = {};
this.effects = {};
this.store = {};
}
// zoo 初始化方法,傳入每一個模塊的 model
init(models) {
Object.values(models).forEach(item => {
// 遍歷加載每一個 model
this.model(item);
});
// 建立並返回全局 store
return this.createStore();
}
// 加載模塊 model 方法
model(modelObj) {
const { state, reducer, effects, namespace } = modelObj;
// 全局保存 state
this.state[namespace] = state;
this.models[namespace] = modelObj;
// 保存 reducer
const newReducer = addNamespace(reducer, namespace);
this.reducers[namespace] = newReducer;
// 保存 effects
this.effects[namespace] = effects;
}
createStore() {
// 合併 reducer, 建立 reducer 函數
const reducer = (state = this.state, action) => {
let newState = state;
const { type, payload } = action;
// 獲取每一個 action 的 namespace
const [namespace, typeName] = type.split('/');
// 根據 namespace 獲取對應 model 中 state 和 reducer 函數對象
const currentState = newState[namespace];
const currentReducer = this.reducers[namespace];
// 若是 action 對應 reducer 存在,則根據函數修改 state,不然直接返回原 state
if (currentReducer && currentReducer[type] && currentState) {
// 根據 reducer 函數修改當前 namespace 的 state
newState[namespace] = currentReducer[type](payload, currentState);
// 修改後的 state 必須是新的對象,這樣纔不會覆蓋舊的 state,可使修改生效
newState = { ...newState };
}
return newState;
};
// 調用 redux createStore 建立 store
this.store = createStore(reducer);
const { dispatch, getState } = this.store;
/** * 給每一個 model 的 effects 對象添加全局 store 的 dispatch、getState 方法 * 用於在 effects 中調用 dispatch * 同時對 effects 中的方法名添加 namespace, 用於組件中 dispatch 時區分模塊 */
Object.keys(this.effects).forEach(namespace => {
this.effects[namespace].dispatch = ({ type, payload }) =>
// 修改 action type,添加 namespace
dispatch({ type: `${namespace}/${type}`, payload });
this.effects[namespace].getState = getState;
});
return this.store;
}
}
export default new Zoo();
複製代碼
import React from 'react';
import { connect } from 'react-redux';
import zoo from './zoo';
// effectsArr 可做爲 effects 依賴注入使用
export default (mapState, mapDispatch = {}, effectsArr = []) => {
return Component => {
const { getState, dispatch } = zoo.store;
// 修改組件中的 dispatch 默認先觸發 effects 中對應方法,不存在時做爲正常 action dispatch
const myDispatch = ({ type, payload }) => {
const [typeId, typeName] = type.split('/');
const { effects } = zoo;
if (effects[typeId] && effects[typeId][typeName]) {
return effects[typeId][typeName](payload);
}
dispatch({ type, payload });
};
const NewComponent = props => {
const { effects } = zoo;
const effectsProps = {};
// 組件中擴展加入 effects 對象,更方便調用 effects 中的方法
effectsArr.forEach(item => {
if (effects[item]) {
effectsProps[`${item}Effects`] = effects[item];
myDispatch[`${item}Effects`] = effects[item];
}
});
return <Component {...props} dispatch={myDispatch} {...effectsProps} />; }; return connect(mapState, mapDispatch)(NewComponent); }; }; 複製代碼
如上,封裝後的 connect 擴展了不少功能,組件中得到的 dispatch 再也不僅僅觸發 action,而是直接 調用 effects 中的方法,更方便反作用處理,同時增長了 effects 依賴注入的接口(相似 Mobx 中的 inject)。緩存
zoo 實現完成,zoo 建立的 store 和 redux 原生建立的 store 並無區別。框架
import { Provider } from 'react-redux';
import zoo from './zoo';
import todoModel from './zooExample/Todo/model';
import zooModel from './zooExample/Zoo/model';
import ZooExample from './zooExample/index';
// 只須要傳入各模塊 model 便可
const zooStore = zoo.init({
todoModel,
zooModel
});
render(
<Provider store={zooStore}> <ZooExample /> </Provider>,
document.getElementById('root')
);
複製代碼
export default {
namespace: 'zoo',
state: {
list: []
},
effects: {
setState(payload) {
const state = this.getState();
this.dispatch({ type: 'setState', payload: payload });
},
addAnimal(name) {
const { list } = this.getState().zoo;
this.setState({ list: [...list, name] });
},
async deleteOne() {
const { list } = this.getState().zoo;
const res = [...list];
// 模擬異步請求操做
setTimeout(() => {
res.pop();
this.setState({ list: res });
}, 1000);
}
},
reducer: {
setState: (payload, state) => ({ ...state, ...payload })
}
};
複製代碼
import React, { useState, useEffect } from 'react';
import { connect } from '../../zoo';
const TestTodo = ({ dispatch, list, zooEffects }) => {
const [value, setValue] = useState('');
useEffect(() => {
dispatch({ type: 'zoo/getAnimal' });
}, []);
const onAdd = () => {
dispatch({
type: 'zoo/addAnimal',
payload: value
});
};
const onDelete = () => {
zooEffects.deleteOne();
// 或 dispatch.zooEffects.deleteOne();
};
return (
<div> <input onChange={e => setValue(e.target.value)} /> <button onClick={onAdd}>add animal</button> <button onClick={onDelete}>delete animal</button> <br /> <ul> {list.map((item, i) => { return <li key={item + i}>{item}</li>; })} </ul> </div> ); }; export default connect( state => { return { list: state.zoo.list }; }, {}, // effects 注入 ['todo', 'zoo'] )(TestTodo); 複製代碼
一個簡易的 redux 封裝就完成了,約 100 多行代碼,不須要寫 action,不須要寫 switch case,相比 dva 異步請求簡單,dispatch 功能強大,能夠實現 3 種方法觸發 effects 也能夠很簡潔。異步
nuomi 中 redux 的封裝原理基本一致async
以上代碼能夠正常使用,但 connect 方法的封裝存在 ref 穿透等細節問題ide
示例代碼倉庫 github.com/iblq/zoo