dva
是蚂蚁金服推出的一个单页应用框架,对redux
,react-router
,redux-saga
进行了上层封装,没有引入新的概念,可是极大的程度上提高了开发效率;下面内容为本人理解,若有错误,还请指出,不胜感激。react
redux
的优势不少,痛点也有,好比异步控制,redux-saga
的出现使得异步操做变得优雅,可是基于redux-saga
不得不认可的一点就是开发过程实在是太麻烦了,倘若增长一个操做,不得不操做actions
,reducers
,sagas
,对于sagas
能够还须要进行watch
,然后还要进行fork
;(PS: 原本就够麻烦了,再加上一个sagas
);在添加一个操做时,不得不操做这么多的文件,实在是麻烦,而dva
的出如今必定程度上解决了这个问题。redux
未使用dva
下的目录常常是这样的:api
actions --/ user.js --/ team.js reducers --/ user.js --/ team.js sagas/ --/ user.js --/ team.js
dva
将其合并:数组
models --/ user.js --/ team.js
dva
中有着几个概念:react-router
namespace => combineReducers中对应的key值 state => 对应初始的state,也就是initialState effects => saga的处理函数 reducers => 对应reducers,不一样的是,写法上将switch...case转化为对象
除了这些之外,dva
中还有subscriptions
,这一律念来源于elm
,app
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
暴露出来的方法:dom
app.router()
:指定路由,须要传入一个函数,通常相似于({ history }) => (<Router>...</Router>)
异步
app.use()
:添加插件,这个稍后来看~ide
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
):
参数校验,是否为DOM
元素或者检查是否能够根据传入的选择器字符串找到对应的DOM
,这个DOM
对应的就是ReactDOM.render
的第二个参数。
错误处理,使得发生错误时,不至于应用奔溃,固然须要传入自定义hooks.onError
来处理:
// 传入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); } };
遍历_models
,初始化reducers,sagas
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)); }
对于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); };
看完了对于reducers
的处理,下面来看一下对于sagas
的处理:
function getSaga(effects, model, onError) { return function *() { for (const key in effects) { if (Object.prototype.hasOwnProperty.call(effects, key)) { const watcher = getWatcher(key, effects[key], model, 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
redux
中间件,由sagaMiddware
,routerMiddware
(启用react-router
时),hooks.onAction
传入的其它中间件,如redux-logger
等
其它加强,如redux-devtools
,内置了redux-devtools
,另需的话在hooks.extraReducers
传入
const enhancers = [ applyMiddleware(...middlewares), devtools(), ...extraEnhancers, ]; const store = this._store = createStore( // eslint-disable-line createReducer(), initialState, compose(...enhancers), );
经过配置hooks.onStateChange
能够指定redux
的state
改变后所触发的回调函数:
const listeners = plugin.get('onStateChange'); for (const listener of listeners) { store.subscribe(() => { listener(store.getState()); }); } }
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)
。
dva.model
与dva.unmodel
,封装了在运行时的store
进行一类增长和删除的操做,例如能够再切换到某一路由时动态的加入一个model
(我的猜想,热更新颇有可能也利用了这个两个api
与hooks.onHmr
)。
关于redux
还有一个利器,那就是高阶reduce
,固然在dva
中也有体现,这篇文章已经很长了,这些内容留在下一篇中介绍。以上是本人对于dva
的粗略的理解,内容若有错误,还请你们指出。dva
的确简化了开发的流程,并且在蚂蚁金服的不少业务线也有着应用,是一个很值得你们一试!