(图片来自Dva官网)
原文再续,书接上回,在 Dva源码解析系列之dva项目入口文件 一文的最后讲到dva是个函数,它返回了app对象,那它是怎样生成app对象的呢?
dva 的源码核心部分包含两部分, dva 和 dva-core。其中 dva 部分负责处理对外的逻辑,使用 react-redux 的Provider实现了View层。它创建了一个挂载了model、router、start等dva所有属性和方法的 app 对象,在最后通过 start 方法启动了一个React应用。接下来,我们就解读dva部分的源码,看看 dva 是如何生成 app 对象的。
生成 app 对象
在 dva/src/index.js 中,导出了一个匿名函数,当dva项目的入口文件index.js 在执行 const app = dva(); 这段代码时,执行的就是 dva/src/index.js 中的这个匿名函数,我们来看看这个匿名函数的源码:
// 函数返回了一个 调用 dva-core 的 create 方法创建的 app 对象,该对象挂载了 model、router、start 等 dva 所有的属性和方法,其中 start 方法是应用启动方法
export default function(opts = {}) {
// 初始化配置
const history = opts.history || createHashHistory();
const createOpts = {
//初始化react-touter
initialReducer: {
router: connectRouter(history),
},
// 将 redux-router 的中间件排在中间件的第一个
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
// 将 history 代理到 app 对象上
setupApp(app) {
// patchHistroy(history)可以监听history变动,从而触发回调
app._history = patchHistory(history);
},
};
// 调用 dva-core 的 crate 方法创建一个 app 对象
const app = create(opts, createOpts);
// 新建变量指向 App 对象希望代理的方法
const oldAppStart = app.start;
// 给App对象挂载路由注册方法,在 dva 项目的入口文件中会调用该方法注册路由
app.router = router;
// 给 App 对象挂载 start 方法,完成 App 对象的 start 方法的代理,在 dva 项目的入口文件中会调用该方法启动应用程序
app.start = start;
return app;
// 路由注册方法
function router(router) {
invariant(
isFunction(router),
`[app.router] router should be function, but got ${typeof router}`,
);
app._router = router;
}
function start(container) {
// 允许 container 是字符串,然后用 querySelector 找元素
if (isString(container)) {
// container 是根元素,通过 querySelector 方法查找根元素
container = document.querySelector(container);
invariant(container, `[app.start] container ${container} not found`);
}
// 并且是 HTMLElement
invariant(
!container || isHTMLElement(container),
`[app.start] container should be HTMLElement`,
);
// 路由必须提前注册
invariant(app._router, `[app.start] router must be registered before app.start()`);
if (!app._store) {
// 使用 call 方法,指定 oldStart 的调用者为 app
oldAppStart.call(app);
}
// 在调用 dva-core 的 create 方法实例化 app 对象时,app 对象中已经有 _store 属性了
const store = app._store;
// export _getProvider for HMR
// ref: https://github.com/dvajs/dva/issues/469
app._getProvider = getProvider.bind(null, store, app);
// If has container, render; else, return react component
if (container) {
// 查找到根元素,将DOM元素插入到根元素中,然后渲染出来
render(container, store, app, app._router);
app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
// 没有找到根元素,返回一个 以 Provider 为根节点的 React component,供外界调用
return getProvider(store, this, this._router);
}
}
}
在这个匿名函数中,调用了 dva-core 中的 create 方法来创建一个 app 对象,然后新建了一个 oldAppStart 变量存放app对象原有的 start 方法,接着给 app 对象挂载了路由注册方法,然后再重新给 app 对象挂载在 dva/src/index.js 文件中新定义的 start 方法,并在匿名函数的最后返回了该 app 对象。
在调用 dva-core 的 create 方法时,传递了两个参数 opts 和 createOpts,其中 opts 是使用者添加的控制选项,createOpts 则是初始化了reducer 和 redux 的中间件,createOpts 初始化如下:
const history = opts.history || createHashHistory();
const createOpts = {
//初始化react-touter
initialReducer: {
router: connectRouter(history),
},
// 将 redux-router 的中间件排在中间件的第一个
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
// 将 history 代理到 app 对象上
setupApp(app) {
// patchHistroy(history)可以监听history变动,从而触发回调
app._history = patchHistory(history);
},
};
router()
// 路由注册方法
function router(router) {
invariant(
isFunction(router),
`[app.router] router should be function, but got ${typeof router}`,
);
app._router = router;
}
在 匿名函数 中挂载到 app 对象上的 router 方法的实现很简单,仅仅是将开发者定义的路由直接挂载到了 app 对象的 _router 属性上,并加上了数据类型的校验。
start()
在 dva 项目的入口文件 index.js 中,是通过调用 app 对象的 start 方法来启动应用程序的,app 对象上的 start 方法,也是在匿名函数中挂载的。接下来,我们看看 start 方法的实现:
function start(container) {
// 允许 container 是字符串,然后用 querySelector 找元素
if (isString(container)) {
// container 是根元素,通过 querySelector 方法查找根元素
container = document.querySelector(container);
invariant(container, `[app.start] container ${container} not found`);
}
// 并且是 HTMLElement
invariant(
!container || isHTMLElement(container),
`[app.start] container should be HTMLElement`,
);
// 路由必须提前注册
invariant(app._router, `[app.start] router must be registered before app.start()`);
if (!app._store) {
// 使用 call 方法,指定 oldStart 的调用者为 app
oldAppStart.call(app);
}
// 在调用 dva-core 的 create 方法实例化 app 对象时,app 对象中已经有 _store 属性了
const store = app._store;
// export _getProvider for HMR
// ref: https://github.com/dvajs/dva/issues/469
app._getProvider = getProvider.bind(null, store, app);
// If has container, render; else, return react component
if (container) {
// 查找到根元素,将DOM元素插入到根元素中,然后渲染出来
render(container, store, app, app._router);
app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
// 没有找到根元素,返回一个 以 Provider 为根节点的 React component,供外界调用
return getProvider(store, this, this._router);
}
}
在 dva 项目的入口文件 index.js 中调用 start 方法时,通常都会传递一个DOM元素的 id 属性进来,而这个DOM元素,则是整个组件树将要挂载的根节点。
在 start 方法中,首先通过 DOM 元素的 querySelector 方法查找根元素,如果没有找到就抛出错误信息。然后使用 call 方法将 oldAppStart(在 create 方法中定义的app对象的start属性) 的调用者指定为新创建的 app 对象。然后根据是否查找到根元素来决定是将组件树渲染成DOM还是返回一个 React Component。
// If has container, render; else, return react component
if (container) {
// 查找到根元素,将DOM元素插入到根元素中,然后渲染出来
render(container, store, app, app._router);
app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
// 没有找到根元素,返回一个 以 Provider 为根节点的 React component,供外界调用
return getProvider(store, this, this._router);
}
当找到根元素时,则调用 render 方法将组件树渲染成DOM:
// 渲染 DOM元素
function render(container, store, app, router) {
const ReactDOM = require('react-dom'); // eslint-disable-line
// 使用 React 的 createElement 创建一个新的组件,然后调用 ReactDOM.render 方法将React component 渲染成DOM
ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
可以看到,在 render 方法中,使用 React 的 createElement 方法将注入了 store 的Provider 组件重新创建一个新组件,然后调用ReactDOM.render方法将新创建出来的组件渲染成DOM。
我们再来看看 getProvider 方法:
// 使用高阶组件包裹Provider 组件,然后返回注入了 store 的 Provider 组件
function getProvider(store, app, router) {
const DvaRoot = extraProps => (
<Provider store={store}>{router({ app, history: app._history, ...extraProps })}</Provider>
);
return DvaRoot;
}
getProvider 方法,使用了高阶组件的形式包裹了react-redux 的 Provider 组件,然后返回注入了 store 的Provider 组件,实现了 store 与 view层的结合。
小结
dva 的源码核心部分包含两部分, dva 和 dva-core,在 dva 部分的源码中,即 dva/src/index.js 文件中,导出了一个匿名函数,dva 项目入口文件中初始化的 app 对象就是在该匿名函数中调用 dva-core 的 create 方法创建的,并在创建 app 对象的过程中完成了路由注册方法及start方法的挂载,并通过react-redux的Provider组件实现了 store 与 view层的结合。
最后贴上 dva/src/index.js 的源码:
import React from 'react';
import invariant from 'invariant';
import { createBrowserHistory, createMemoryHistory, createHashHistory } from 'history';
import document from 'global/document';
import {
Provider,
connect,
connectAdvanced,
useSelector,
useDispatch,
useStore,
shallowEqual,
} from 'react-redux';
import { bindActionCreators } from 'redux';
import { utils, create, saga } from 'dva-core';
import * as router from 'react-router-dom';
import * as routerRedux from 'connected-react-router';
const { connectRouter, routerMiddleware } = routerRedux;
const { isFunction } = utils;
const { useHistory, useLocation, useParams, useRouteMatch } = router;
// 函数返回了一个 调用 dva-core 的 create 方法创建的 app 对象,该对象挂载了 model、router、start 等 dva 所有的属性和方法,其中 start 方法是应用启动方法
export default function(opts = {}) {
// 初始化配置
const history = opts.history || createHashHistory();
const createOpts = {
//初始化react-touter
initialReducer: {
router: connectRouter(history),
},
// 将 redux-router 的中间件排在中间件的第一个
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
// 将 history 代理到 app 对象上
setupApp(app) {
// patchHistroy(history)可以监听history变动,从而触发回调
app._history = patchHistory(history);
},
};
// 调用 dva-core 的 crate 方法创建一个 app 对象
const app = create(opts, createOpts);
// 新建变量指向 App 对象希望代理的方法
const oldAppStart = app.start;
// 给App对象挂载路由注册方法,在 dva 项目的入口文件中会调用该方法注册路由
app.router = router;
// 给 App 对象挂载 start 方法,完成 App 对象的 start 方法的代理,在 dva 项目的入口文件中会调用该方法启动应用程序
app.start = start;
return app;
// 路由注册方法
function router(router) {
invariant(
isFunction(router),
`[app.router] router should be function, but got ${typeof router}`,
);
app._router = router;
}
function start(container) {
// 允许 container 是字符串,然后用 querySelector 找元素
if (isString(container)) {
// container 是根元素,通过 querySelector 方法查找根元素
container = document.querySelector(container);
invariant(container, `[app.start] container ${container} not found`);
}
// 并且是 HTMLElement
invariant(
!container || isHTMLElement(container),
`[app.start] container should be HTMLElement`,
);
// 路由必须提前注册
invariant(app._router, `[app.start] router must be registered before app.start()`);
if (!app._store) {
// 使用 call 方法,指定 oldStart 的调用者为 app
oldAppStart.call(app);
}
// 在调用 dva-core 的 create 方法实例化 app 对象时,app 对象中已经有 _store 属性了
const store = app._store;
// export _getProvider for HMR
// ref: https://github.com/dvajs/dva/issues/469
app._getProvider = getProvider.bind(null, store, app);
// If has container, render; else, return react component
if (container) {
// 查找到根元素,将DOM元素插入到根元素中,然后渲染出来
render(container, store, app, app._router);
app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
// 没有找到根元素,返回一个 以 Provider 为根节点的 React component,供外界调用
return getProvider(store, this, this._router);
}
}
}
function isHTMLElement(node) {
return typeof node === 'object' && node !== null && node.nodeType && node.nodeName;
}
function isString(str) {
return typeof str === 'string';
}
// 使用高阶组件包裹Provider 组件,然后返回注入了 store 的 Provider 组件
function getProvider(store, app, router) {
const DvaRoot = extraProps => (
<Provider store={store}>{router({ app, history: app._history, ...extraProps })}</Provider>
);
return DvaRoot;
}
// 渲染 DOM元素
function render(container, store, app, router) {
const ReactDOM = require('react-dom'); // eslint-disable-line
// 使用 React 的底层方法 createElement 创建一个新的组件,然后调用 ReactDOM.render 方法将React component 渲染成HTML
ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
//使用代理模式扩展 history 对象的 listen 方法,添加了一个回调函数做参数并在路由变化时主动调用
function patchHistory(history) {
const oldListen = history.listen;
history.listen = callback => {
// TODO: refact this with modified ConnectedRouter
// Let ConnectedRouter to sync history to store first
// connected-react-router's version is locked since the check function may be broken
// min version of connected-react-router
// e.g.
// function (e, t) {
// var n = arguments.length > 2 && void 0 !== arguments[2] && arguments[2];
// r.inTimeTravelling ? r.inTimeTravelling = !1 : a(e, t, n)
// }
// ref: https://github.com/umijs/umi/issues/2693
const cbStr = callback.toString();
const isConnectedRouterHandler =
(callback.name === 'handleLocationChange' && cbStr.indexOf('onLocationChanged') > -1) ||
(cbStr.indexOf('.inTimeTravelling') > -1 &&
cbStr.indexOf('.inTimeTravelling') > -1 &&
cbStr.indexOf('arguments[2]') > -1);
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;
}
export fetch from 'isomorphic-fetch';
export dynamic from './dynamic';
export { connect, connectAdvanced, useSelector, useDispatch, useStore, shallowEqual };
export { bindActionCreators };
export { router };
export { saga };
export { routerRedux };
export { createBrowserHistory, createMemoryHistory, createHashHistory };
export { useHistory, useLocation, useParams, useRouteMatch };