写在前面
dva 是蚂蚁金服推出的一个单页应用框架,对redux ,react-router ,redux-saga 进行了上层封装,没有引入新的概念,但是极大的程度上提升了开发效率;下面内容为本人理解,如有错误,还请指出,不胜感激。
redux的痛苦
redux 的优点很多,痛点也有,比如异步控制,redux-saga 的出现使得异步操作变得优雅,但是基于redux-saga 不得不承认的一点就是开发过程实在是太麻烦了,假若增加一个操作,不得不操作actions ,reducers ,sagas ,对于sagas 可以还需要进行watch ,而后还要进行fork ;(PS: 本来就够麻烦了,再加上一个sagas );在添加一个操作时,不得不操作这么多的文件,实在是麻烦,而dva 的出现在一定程度上解决了这个问题。
dva基本概念
未使用dva 下的目录经常是这样的:
actions
--/ user.js
--/ team.js
reducers
--/ user.js
--/ team.js
sagas/
--/ user.js
--/ team.js
dva 将其合并:
models
--/ user.js
--/ team.js
dva 中有着几个概念:
namespace => combineReducers中对应的key值
state => 对应初始的state,也就是initialState
effects => saga的处理函数
reducers => 对应reducers,不同的是,写法上将switch...case转化为对象
除了这些以外,dva 中还有subscriptions ,这一概念来源于elm ,
dva的实现
初始化
const app = dva({
history: browserHistory
});
上面的过程发生了什么?
dva 本质上调用了下面函数:
function dva(hooks = {}) {
const history = hooks.history || defaultHistory;
const initialState = hooks.initialState || {};
delete hooks.history;
delete hooks.initialState;
const plugin = new Plugin();
plugin.use(hooks);
const app = {
// properties
_models: [],_router: null,_store: null,_history: null,_plugin: plugin,_getProvider: null,// methods
use,model,router,start,};
return app;
}
hooks 为传入的一些配置,例如可以通过传入history 来改变路由的实现,dva 默认采用的是hashHistory ;从这里可以看出dva 暴露出来的方法:
app.router() :指定路由,需要传入一个函数,一般类似于({ history }) => (<Router>...</Router>)
app.use() :添加插件,这个稍后来看~
-
app.model() :添加model ,也就是对应的添加一个store 下的数据,该方法做的就是对传入的model 进行检查,对reducers 添加命名空间,而后将其push 到_models 中。
namespace 必须且唯一,因为内置了react-redux-router ,所以namespace 也不能为routing
subscriptions 与effects 均为可选参数,传入的话必须为对象
reducers 为可选,支持对象和数组两种传入方式(传入数组的方式,往往伴随着高阶reducer 的应用,具体稍后再看~)
app.start() :初始化应用,接受参数为选择器或者DOM 节点
需要注意的是:
reducers 和effects 的key 不需要用namespace/action 的形式了,因为dva 会自动将其加上,dispatch 的时候,saga 需要加上namespace ,而saga 中的put 不需要加入namespace ,原因是dva 对put进行了重载
dva 同时支持rn应用,引入dva/mobile 即可,这时react-router 不在需要,利用rn中的Navigator 即可,不会引用react-router 与react-redux-router ,namespace 可以命名为routing ;正是由于这点差异,作者将路由相关的内容作为参数传入了进去,具体可以参见这个文件。
创建
将一些配置项初始化好后,就可以app.start 就是来创建一个应用,下面就一点点的看看start 的过程(以下基于默认情况,也就是使用了react-router ):
// 传入hooks.onError则调用,反之调用默认函数处理,抛出异常即可
const onError = plugin.apply('onError',(err) => {
throw new Error(err.stack || err);
});
// 目的是出现错误时,也可以进行dispatch操作
const onErrorWrapper = (err) => {
if (err) {
if (typeof err === 'string') err = new Error(err);
onError(err,app._store.dispatch);
}
};
const sagas = [];
// initalReducer为{ routing: routerReducer }
const reducers = { ...initialReducer }; // 为rootReducer
for (const m of this._models) {
// 得到默认的state
reducers[m.namespace] = getReducer(m.reducers,m.state);
if (m.effects) sagas.push(getSaga(m.effects,m,onErrorWrapper));
}
处理reducers
对于redux 的reducers 最常见的是基于switch..case 的,而dva 做出了一些改变,将每一个case 分支变作了一个函数:
(PS: 本人认为,这个可以块可以更改,利用some 操作来尽可能少的调用无意义的reducer ,于是我提了一个pr)
每一个reducer 的实现如下:
// actionType对应的是dva的reducers中的key值
(state,action) => {
const { type } = action;
if (type && actionType !== type) {
return state;
}
return reducer(state,action);
};
处理sagas
看完了对于reducers 的处理,下面来看一下对于sagas 的处理:
function getSaga(effects,onError) {
return function *() {
for (const key in effects) {
if (Object.prototype.hasOwnProperty.call(effects,key)) {
const watcher = getWatcher(key,effects[key],onError);
const task = yield sagaEffects.fork(watcher);
// 为了移除时可以将saga任务注销
yield sagaEffects.fork(function *() {
yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
yield sagaEffects.cancel(task);
});
}
}
};
}
getWatcher 返回一个saga 监听函数,也就是通常写的watchXXX ,model.effects[key] 可以是一个任务函数;也可以是个数组,第一个参数为任务函数,第二为配置对象,可以传入type ,type 有4个可选值,takeEvery (默认),takeLatest ,throttle ,watcher 四种,dva 对effects 做了一个错误处理:
effect => function *(...args) {
try {
yield effect(...args.concat(createEffects(model)));
} catch (e) {
onError(e); // 为之前的onErrorWrapper
}
}
注意:
watcher 是指传入的任务函数就是一个watcher 直接fork 就好
throttle 还要传入一个ms 配置,这个ms 代表着在多少毫秒内只触发一次同一类型saga 任务,而takeEvery 是不会限制同一类型执行次数,takeLatest 只能执行一个同一类型任务,有执行中的再次执行就会取消
由getSaga 可以看出,${namespace}/@@CANCEL_EFFECTS 可以取消对应的任务监听
可以通过配置hooks.onEffect 来增加saga 的watcher
增强redux
const enhancers = [
applyMiddleware(...middlewares),devtools(),...extraEnhancers,];
const store = this._store = createStore( // eslint-disable-line
createReducer(),initialState,compose(...enhancers),);
设置redux的回调函数
通过配置hooks.onStateChange 可以指定redux 的state 改变后所触发的回调函数:
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
store.subscribe(() => {
listener(store.getState());
});
}
}
新概念subscriptions
subscriptions 是一个新概念,会在dom ready 之后执行,在这里面可以做一些基础数据的获取: 一般会将初始数据的获取放在react 的生命周期中,比如componentWillMount ,但是假设我们做了代码分割,实现了按需加载,那么我们开始获取数据的时间为:获取相应的js +解析js +执行react 生命周期,但是redux 的数据加载和ui 组件没有太大关系,可以将数据获取的时间点提前,subscriptions 提供了解决方法,其意义为订阅,对于上面的场景,我们可以订阅路由,到了执行的路由执行相应的dispatch() ,如:
setup({ dispatch,history }) {
return history.listen(({ pathname,query }) => {
if (pathname === '/users') {
dispatch({ type: 'fetch',payload: query });
}
});
}
(PS: 对于这个新概念,我也不是很清楚,后面的文章会有专门的描述,大家先有一个概念就好)
挂载
上述过程均为初始化的过程,就是获取到需要的reducers ,sagas 以及对于一些中间件和插件的配置,下面要进行的就是挂载了,也就熟悉的render(<Provider>,container) 。
动态处理model
dva.model 与dva.unmodel ,封装了在运行时的store 进行一类增加和删除的操作,例如可以再切换到某一路由时动态的加入一个model (个人猜测,热更新很有可能也利用了这个两个api 与hooks.onHmr )。
未完结
关于redux 还有一个利器,那就是高阶reduce ,当然在dva 中也有体现,这篇文章已经很长了,这些内容留在下一篇中介绍。以上是本人对于dva 的粗略的理解,内容如有错误,还请大家指出。dva 的确简化了开发的流程,而且在蚂蚁金服的很多业务线也有着应用,是一个很值得大家一试! (编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|