dva源码分析
dva是基于react的二次封装,在工作中经常用到,因此有必要了解一下其中的实现。本文将从源码层面分析dva是如何将redux
、redux-saga
、react-router
等进行封装的。
本文使用的dva版本为2.6.0-beta.20
,参考
dva构建流程
下面是一个简单的dva应用
// 初始化应用
const app = dva({
onError(error) {
console.log(error)
}
})
app.use(createLoading()) // 使用插件
app.model(model) // 注册model
app.router(App) // 注册路由
app.start('#app') // 渲染应用
我们从这几个api开始,了解dva是如何封装react应用的。
整个dva项目使用lerna管理的,在每个package的package.json中找到模块对应的入口文件,然后查看对应源码。
dva start
// dva/src/index.js
export default function(opts = {}) {
const history = opts.history || createHashHistory();
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
const app = create(opts, createOpts);
const oldAppStart = app.start;
app.router = router;
app.start = start;
return app;
// 为app暴露的router接口,主要是更新app._router
function router(router) {
app._router = router;
}
// 为app暴露的start接口,主要渲染整个应用
function start(container) {
// 根据container找到对应的DOM节点
if (!app._store) oldAppStart.call(app); // 执行原本的app.start
const store = app._store;
// 为HMR暴露_getProvider接口
app._getProvider = getProvider.bind(null, store, app);
// If has container, render; else, return react component
if (container) {
render(container, store, app, app._router);
app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
return getProvider(store, this, this._router);
}
}
}
// 渲染逻辑
function getProvider(store, app, router) {
const DvaRoot = extraProps => (
// app.router传入的组件在此处被渲染,并传入app,history等prop
<Provider store={store}>{router({ app, history: app._history, ...extraProps })}</Provider>
);
return DvaRoot;
}
function render(container, store, app, router) {
const ReactDOM = require('react-dom');
ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
可以看见app是通过create(opts, createOpts)
进行初始化的,其中opts
是暴露给使用者的配置,createOpts
是暴露给开发者的配置,真实的create
方法在dva-core
中实现。
dva-core craete
下面是create
方法的大致实现
// dva-core/src/index.js
const dvaModel = {
namespace: '@@dva',
state: 0,
reducers: {
UPDATE(state) {
return state + 1;
},
},
};
export function create(hooksAndOpts = {}, createOpts = {}) {
const { initialReducer, setupApp = noop } = createOpts; // 在dva/index.js中构造了createOpts对象
const plugin = new Plugin(); // dva-core中的插件机制,每个实例化的dva对象都包含一个plugin对象
plugin.use(filterHooks(hooksAndOpts)); // 将dva(opts)构造参数opts上与hooks相关的属性转换成一个插件
const app = {
_models: [prefixNamespace({ ...dvaModel })],
_store: null,
_plugin: plugin,
use: plugin.use.bind(plugin), // 暴露的use方法,方便编写自定义插件
model, // 暴露的model方法,用于注册model
start, // 原本的start方法,在应用渲染到DOM节点时通过oldStart调用
};
return app;
}
可以看见上面代码主要暴露了use
、model
和start
这三个接口。
插件系统
dva实现高度依赖内置的Plugin系统,为了便于理解后续代码,我们暂时跳出start方法,去探究Plugin原理。
dva的插件时一个包含下面部分属性的JS对象,每个属性对应一个函数或配置项
// plugin对象只暴露了下面这几个属性,顾名思义,以on开头的方法会在整个应用的某些时刻触发,类似于生命周期钩子函数
const hooks = [
'onError',
'onStateChange',
'onAction',
'onHmr',
'onReducer',
'onEffect',
'extraReducers',
'extraEnhancers',
'_handleActions',
];
一个大概的插件结构类似于
const testPlugin = {
onStateChange(newState){
console.log(newState)
}
// ...其他需要监听的hook
}
app.use(testPlugin)
来看看在start初始化时构造的Plugin管理器
// Plugin管理对象
class Plugin {
constructor() {
this._handleActions = null;
// 每个钩子对应的处理函数默认都是空数组
this.hooks = hooks.reduce((memo, key) => {
memo[key] = [];
return memo;
}, {});
}
// 接收一个plugin对象
use(plugin) {
const { hooks } = this;
for (const key in plugin) {
if (Object.prototype.hasOwnProperty.call(plugin, key)) {
// ...检测use的plugin上的key,并处理特定形式的key
if (key === '_handleActions') {
this._handleActions = plugin[key]; // 如果定义了插件的_handleActions方法,会覆盖Plugin对象本身的_handleActions
} else if (key === 'extraEnhancers') {
hooks[key] = plugin[key]; // 如果定义了插件的extraEnhancers方法,会覆盖this.hooks本身的extraEnhancers
} else {
hooks[key].push(plugin[key]); // 其他key对应的方法会追加到hooks对应key数组中
}
}
}
}
// 返回一个通过key执行hooks上注册的所有方法的函数
apply(key, defaultHandler) {
const { hooks } = this;
const fns = hooks[key];
// 遍历fns并以此执行
return (...args) => {};
}
// 根据key找到hooks上对应的方法
get(key) {
const { hooks } = this;
if (key === 'extraReducers') {
// 将hooks[key]都合并到一个对象中,如将[{ x: 1 }, { y: 2 }]转换为{x:1,y:2}的对象
return getExtraReducers(hooks[key]);
} else if (key === 'onReducer') {
// 返回一个接受reducer并依次运行hooks.onReducer的函数,每次循环都会将前一个hooks.onReducer元素方法返回值作为新的reducer
return getOnReducer(hooks[key]);
} else {
return hooks[key];
}
}
}
function getOnReducer(hook) {
return function(reducer) {
for (const reducerEnhancer of hook) {
reducer = reducerEnhancer(reducer);
}
return reducer;
};
}
可以看见,整个插件系统的实现还是比较简单的,主要暴露了use
注册插件、get
根据key获取插件等接口。
app.use = plugin.use.bind(plugin)
初始化Store
回到前面章节,继续查看start方法的执行流程,我们可以看见完成的store构建流程
// oldStart方法
function start() {
// 全局错误处理函数,如果某个插件注册了onError钩子,将在此处通过plugin.apply('onError')调用
const onError = (err, extension) => {};
// 遍历app._models,收集saga
app._getSaga = getSaga.bind(null);
const sagas = [];
const reducers = { ...initialReducer };
for (const m of app._models) {
reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
if (m.effects) {
sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
}
// 创建 app._store,具体参看下面章节的createStore
// app._store = craeteStore()...
const store = app._store;
// Extend store
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {};
// Execute listeners when state is changed
// 获取所有监听了onStateChange的插件,通过store.subscribe注册到redux中
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
store.subscribe(() => {
listener(store.getState());
});
}
// 运行所有sagas
sagas.forEach(sagaMiddleware.run);
setupApp(app);
// 运行所有model的subscriptions方法
const unlisteners = {};
for (const model of this._models) {
if (model.subscriptions) {
unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
}
}
// 在执行start时注册model、unmodel和replaceModel三个接口,具体参看下面章节
// ...
}
可见在整个start方法中
- 遍历
app._models
,收集并运行sagas
, - 初始化
app._store
,使用store.subscribe
注册所有插件的onStateChange
回调 - 运行所有的
model.subscriptions
- 暴露
app.model
、app.unmodel
、app.replaceModel
三个接口
createStore
createStore
方法是封装的redux createStore
,传入了一些中间件和reducers
const sagaMiddleware = createSagaMiddleware(); // 初始化saga中间件
const promiseMiddleware = createPromiseMiddleware(app);
app._store = createStore({
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware,
});
function createStore({
reducers, initialState, plugin, sagaMiddleware, promiseMiddleware,
createOpts: { setupMiddlewares = returnSelf },
}) {
// extra enhancers
const extraEnhancers = plugin.get('extraEnhancers');
const extraMiddlewares = plugin.get('onAction');
const middlewares = setupMiddlewares([
promiseMiddleware,
sagaMiddleware,
...flatten(extraMiddlewares),
]);
const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];
// redux.createStore
return createStore(reducers, initialState, compose(...enhancers));
}
其中的reducers
参数由createReducer
方法构建,该方法返回一个增强版的reducer
const reducerEnhancer = plugin.get('onReducer'); // 在插件系统中提到,传入onReducer时实际返回所有插件组合后的onReducer方法
const extraReducers = plugin.get('extraReducers'); // 传入extraReducers实际返回的是所有插件合并后的reducerObj对象
function createReducer() {
return reducerEnhancer(
// redux.combineReducers
combineReducers({
...reducers, // start Opts参数上的默认reducer
...extraReducers,
...(app._store ? app._store.asyncReducers : {}),
}),
);
}
其中promiseMiddleware
中间件由createPromiseMiddleware
创建,是一个当isEffect(action.type)
时返回Promise的中间件,并在原本的action上挂载该Promise的{__dva_resolve: resolve, __dva_reject: reject}
export default function createPromiseMiddleware(app) {
return () => next => action => {
const { type } = action;
// isEffect的实现为:从app._models中找到namespace与action.type.split('/')[0]相同的model,然后根据model.effects[type]是否存在判断当前action是否需要返回Promise
if (isEffect(type)) {
return new Promise((resolve, reject) => {
next({
__dva_resolve: resolve,
__dva_reject: reject,
...action,
});
});
} else {
return next(action);
}
};
}
model三接口
一个model大概是下面的结构
{
namespace: 'index_model',
state: {
text: 'hello'
},
effects: {
* asyncGetInfo({payload = {}}, {call, put}) {
}
},
reducers: {
updateData: (state, {payload}) => {
return {...state, ...payload}
}
}
}
在create
方法中,app.model
方法实现如下,其本质是
function model(m) {
// 将model上的reducers和effects各个key处理为`${namespace}${NAMESPACE_SEP}${key}`形式
const prefixedModel = prefixNamespace({ ...m });
app._models.push(prefixedModel);
return prefixedModel;
}
在执行start方法后,会覆盖app.model方法为injectModel
,并新增unmodel
和replaceModel
app.model = injectModel.bind(app, createReducer, onError, unlisteners);
app.unmodel = unmodel.bind(app, createReducer, reducers, unlisteners);
app.replaceModel = replaceModel.bind(app, createReducer, reducers, unlisteners, onError);
// 动态添加model,这样就可以实现model与route组件类似的异步加载方案
function injectModel(createReducer, onError, unlisteners, m) {
m = model(m);
const store = app._store;
store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
// 重新调用createReducer
store.replaceReducer(createReducer());
if (m.effects) {
store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
// 更新unlisteners方便后面移除
if (m.subscriptions) {
unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);
}
}
// 根据namespace移除对应model,需要移除store.asyncReducers并调用store.replaceReducer(createReducer())重新生成reducer
function unmodel(createReducer, reducers, unlisteners, namespace) {}
// 找到namespace相同的model并替换,如果不存在,则直接添加
function replaceModel(createReducer, reducers, unlisteners, onError, m) {}
通过此处了解到,dva中的model分为两类
- 在调用start之前注册的model
- 在调用start之后动态注册的model
路由
在前面的dva.start
方法中我们看到了createOpts
,并了解到在dva-core
的start
中的不同时机调用了对应方法
import * as routerRedux from 'connected-react-router';
const { connectRouter, routerMiddleware } = routerRedux;
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
其中initialReducer
和setupMiddlewares
在初始化store时调用,然后才调用setupApp
可以看见针对router相关的reducer
和中间件配置,其中connectRouter
和routerMiddleware
均使用了connected-react-router
这个库,其主要思路是:把路由跳转也当做了一种特殊的action。
首先来看看connectRouter
方法的实现,该方法会返回关于router的reducer
// connected-react-router/src/reducer.js
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'
const createConnectRouter = (structure) => {
const { fromJS, merge } = structure // 复制和合并JS对象的两个工具方法
const createRouterReducer = (history) => {
const initialRouterState = fromJS({
location: injectQuery(history.location),
action: history.action,
})
return (state = initialRouterState, { type, payload } = {}) => {
if (type === LOCATION_CHANGE) {
const { location, action, isFirstRendering } = payload
// 首次渲染时不更新state
return isFirstRendering
? state
: merge(state, { location: fromJS(injectQuery(location)), action })
}
return state
}
}
return createRouterReducer
}
然后是routerMiddleware
,该方法返回关于router的中间件
export const CALL_HISTORY_METHOD = '@@router/CALL_HISTORY_METHOD'
const routerMiddleware = (history) => store => next => action => {
if (action.type !== CALL_HISTORY_METHOD) {
return next(action)
}
// 当action为路由跳转时,调用history方法而不是执行next
const { payload: { method, args } } = action
history[method](...args)
}
最后回到dva中,查看patchHistory方法
function patchHistory(history) {
// 劫持了history.listen方法
const oldListen = history.listen;
history.listen = callback => {
const cbStr = callback.toString();
// 当使用了dva.routerRedux.ConnectedRouter路由组件时,其构造函数会执行history.listen,此时isConnectedRouterHandler将为true
const isConnectedRouterHandler =
(callback.name === 'handleLocationChange' && cbStr.indexOf('onLocationChanged') > -1) ||
(cbStr.indexOf('.inTimeTravelling') > -1 && cbStr.indexOf('arguments[2]') > -1);
// 在app.start之后的其他地方如model.subscriptions中使用history时,会接收到location和action两个参数
callback(history.location, history.action);
return oldListen.call(history, (...args) => {
if (isConnectedRouterHandler) {
callback(...args);
} else {
// Delay all listeners besides ConnectedRouter
setTimeout(() => {
callback(...args);
});
}
});
};
return history;
}
可以看见,dva对router并未做过多封装,只是通过connected-react-router
提供了一个reducer和一个中间件,并将该库暴露为routerRedux
。
小结
从源码可以看见,dva主要是对redux
和redux-saga
进行封装,简化并对外暴露了几个有限接口。除此之外,还内置了react-router
和其他一些库,因此也可以看做是一个轻量级应用框架
dva
主要对外提供了相关的apidva-core
对redux
和redux-saga
进行封装,并实现了一个简单的插件系统
学习dva源码的一个好处可以让你去了解整个React的生态,并学会使用一种还不错的开发方案。了解dva的封装实现只是学习的第一步,要想编写高效可维护的代码,还需要深入redux
、redux-saga
、react-router
等库的使用和实现。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。