实现功能类似 dva、nuomi、rematch、mirror 的数据流管理方式
-
redux 原生写法实现功能需要写大量的 action、reducer 模板代码,对于复杂项目来说模板代码太多,且 action 文件和 reducer 文件等不停切换,开发体验较差。大家一般都会封装简化。
-
比如 dva 的实现方式,引入了 effects,把 state、effects、action、reducer 放在一个 model 文件,简化了 action 和 reducer 的定义方式。类似的还有 rematch,mirror。
-
今年项目重构时,因路由缓存等原因,没有采用现有 dva 等框架,简易封装实现了类似 dva 的 redux 数据流实现。
简单封装实现下类似 api 和使用方式。
实现代码
- 命名 zoo,因为 z 字母开头文件列表中在最底部,方便查找
直接上代码,实现 redux 主体功能封装
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();
- connect 封装
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 并没有区别。
zoo 使用
- index.js
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')
);
- model.js
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 })
}
};
- 功能组件 ZooExample.js
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 的封装原理基本一致
以上代码可以正常使用,但 connect 方法的封装存在 ref 穿透等细节问题
示例代码仓库 github.com/iblq/zoo