一. 先从官方快速上手 dva-cli
说起,建立起工程,参考 dva官网, 然后打开 index.js
看到下面, 其中有五个步骤,下面就一一分析
import dva from 'dva';
import './index.css';
// 1. Initialize
const app = dva();
// 2. Plugins
app.use({});
// 3. Model
app.model(require('./models/products').default);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root');
1. Initialize :const app = dva();
初始化了一个dva对象,跳转到源码index.d.ts
,发现是下面TS的声明文件,学习了发现这跟c++头文件一样,这是对外接口的描述,TS里面只需要把d.ts
和源文件放一个目录就可以都不需要引用,比如这里和index.js
放一起
export interface DvaInstance {
use: (hooks: Hooks) => void,
model: (model: Model) => void,
unmodel: (namespace: string) => void,
router: (router: Router) => void,
start: (selector?: HTMLElement | string) => any,
}
export default function dva(opts?: DvaOption): DvaInstance;
那么具体实现部分就看看index.js
, 又是一个引用,应该是为了方便组织代码,我们就看./lib
下面的文件,注意上面的export default
, 我们要找这个default输出
module.exports = require('./lib');
module.exports.connect = require('react-redux').connect;
打开./lib
文件夹,发现怎么有个dynamic.js
, 查了资料也不知道搞嘛的,发现有个redux-container-builder
里面有defaultLoadingComponent
这里面用到了,应该是动态加载模块什么,暂时没搞清楚,可以先不管,直接看里面的index.js
, 找到export default
就是你需要的dva
, 发现跟上面的声明文件对上了,少了些对象,应该是core.create
创建的,
export default function (opts = {}) {
// ...省略
const app = core.create(opts, createOpts);
const oldAppStart = app.start;
app.router = router;
app.start = start;
return app;
// ...省略
}
找到dva-core
里面的实现index.js
, 可以看到dva完整啦,这下有dva
有了use, model, router, start
几个关键属性,没错,接下的步骤就是调用这些属性方法的呀!!
这里把
dva-core
独立应该是为了隔离react
,dva-core
里面是针对react
的实现,这样dva
就变成一个代码组织的框架,利于以后复用
function create() {
//...省略
var app = {
_models: [(0, _prefixNamespace.default)((0, _objectSpread2.default)({}, dvaModel))],
_store: null,
_plugin: plugin,
use: plugin.use.bind(plugin),
model: model,
start: start
};
return app;
//...省略
}
2. Plugins: app.use({});
经过上面初始化,已经拿到dva
生成的app
对象啦, 由于use
方法在dva-core
中,我们就看这里面use
了什么,下面关键代码就是定义一个数组hook
钩子,经过reduce
(什么是reduce? 那你应该百度了)后,每个事件都是一个数组,use
的作用就是遍历插件for (const key in plugin)
,把相应的插件实现的方法放入这个数组hooks[key].push(plugin[key]);
,留做以后调用.
什么是插件: 简单的说在应用跑起来后会有各种生命周期,比如当出错时
onError
, 当发起请求时onEffect
, 不同的应用对于这些不同时机想要做的事是不一样的,怎么解决这一问题呢,这就是插件的由来,dva
在各时机进行拦截并取好名字,如hook
里面的名字,用户按照规范写插件,来一一触发,比如官方的dva-loading
, 就在注册了在onEffect
时候打开和关闭遮罩,具体可看源码
const hooks = [
'onError',
'onStateChange',
'onAction',
'onHmr',
'onReducer',
'onEffect',
'extraReducers',
'extraEnhancers',
'_handleActions',
];
// ...省略
export default class Plugin {
constructor() {
this._handleActions = null;
this.hooks = hooks.reduce((memo, key) => {
memo[key] = [];
return memo;
}, {});
}
use(plugin) {
invariant(
isPlainObject(plugin),
'plugin.use: plugin should be plain object'
);
const hooks = this.hooks;
for (const key in plugin) {
if (Object.prototype.hasOwnProperty.call(plugin, key)) {
invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);
if (key === '_handleActions') {
this._handleActions = plugin[key];
} else if (key === 'extraEnhancers') {
hooks[key] = plugin[key];
} else {
hooks[key].push(plugin[key]);
}
}
}
}
拿dva-loading
插件举例,比如app.use(createLoading())
后, onEffect
里面就被压入了实现的函数内容,压入后的hook
对象如下,等到要用的时候dva
内部会顺序调用,这样插件的准备工作就做好了.
hooks = {
extraEnhancers: [],
extraReducers: [],
onAction: [],
onEffect: [
function onEffect(effect, { put }, model, actionType) {
// dva-laoding 里面实现的部分
},
],
onError: [],
onHmr: [],
onReducer: [],
onStateChange: [],
_handleActions: [],
}
3. Model : app.model(require('./models/products').default);
按照官方的步骤,这里的products
建立如下
export default {
namespace: "products",
state : [],
reducers: {
"delete"(state,{payload: id}) {
return state.filter(item => item.id !== id);
}
}
};
来到dva-core
里面看model
的源码,更简单,首先判断下是否满足model
的定义checkModel(m, app._models);
,不满足报错,满足后根据model
里面的namespace
给每个model
加前缀名字const prefixedModel = prefixNamespace({ ...m });
,然后再放入内部的app._models
里面留有后用
function model(m) {
if (process.env.NODE_ENV !== 'production') {
checkModel(m, app._models);
}
const prefixedModel = prefixNamespace({ ...m });
app._models.push(prefixedModel);
return prefixedModel;
}
model
的定义如下
const {
namespace,
reducers,
effects,
subscriptions,
} = model;
4. Router: app.router(require('./router').default);
到了路由了, 源码更简单,直接存了一下, 但是router
是个啥子呢??
function router(router) {
invariant(
isFunction(router),
`[app.router] router should be function, but got ${typeof router}`,
);
app._router = router;
}
一看这结构,是不是跟react-router-dom
里面的很像,没错就是一个模子,查看源码发现就是react-router-dom
里面的路由,名都没改
import React from 'react';
import { Router, Route, Switch } from 'dva/router';
import IndexPage from './routes/IndexPage';
import Products from './routes/Products';
function RouterConfig({ history }) {
return (
<Router history={history}>
<Switch>
<Route path="/" exact component={IndexPage} />
<Route path="/products" exact component={Products} />
</Switch>
</Router>
);
}
export default RouterConfig;
dva
中router.js
文件如下
module.exports = require('react-router-dom');
module.exports.routerRedux = require('react-router-redux');
5. Start: app.start('#root');
先贴上start
的源码, 这是最后也是最重要的一步啦,加油!!
function start(container) {
// 允许 container 是字符串,然后用 querySelector 找元素
if (isString(container)) {
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) {
oldAppStart.call(app);
}
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) {
render(container, store, app, app._router);
app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
return getProvider(store, this, this._router);
}
}
下面我们一步一步分解来看简单的分为以下几小步
- 前面一堆
invariant
只是确保把container
转换成DOM元素,router
注册好没有,至于invariant, 放上官方描述,慢慢体会
A mirror of Facebook's `invariant` (e.g. React, flux).
A way to provide descriptive errors in development but generic errors in production.
-
oldAppStart.call(app);
这步其实很重要,因为首次进来肯定没有app._store
的,这个oldAppStart
其实是dva-core
里面的start
函数,用来调用redux
的createStore
, 放上源码,有点多啊,不想看怎么办,没事,我来给你细细讲解
function start() {
// Global error handler
var onError = function onError(err, extension) {
if (err) {
if (typeof err === 'string') err = new Error(err);
err.preventDefault = function () {
err._dontReject = true;
};
plugin.apply('onError', function (err) {
throw new Error(err.stack || err);
})(err, app._store.dispatch, extension);
}
};
var sagaMiddleware = (0, _middleware.default)();
var promiseMiddleware = (0, _createPromiseMiddleware.default)(app);
app._getSaga = _getSaga.default.bind(null);
var sagas = [];
var reducers = (0, _objectSpread2.default)({}, initialReducer);
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = (0, _getIterator2.default)(app._models), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var m = _step.value;
reducers[m.namespace] = (0, _getReducer.default)(m.reducers, m.state, plugin._handleActions);
if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return != null) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
var reducerEnhancer = plugin.get('onReducer');
var extraReducers = plugin.get('extraReducers');
(0, _invariant.default)((0, _keys.default)(extraReducers).every(function (key) {
return !(key in reducers);
}), "[app.start] extraReducers is conflict with other reducers, reducers list: ".concat((0, _keys.default)(reducers).join(', '))); // Create store
var store = app._store = (0, _createStore.default)({
// eslint-disable-line
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin: plugin,
createOpts: createOpts,
sagaMiddleware: sagaMiddleware,
promiseMiddleware: promiseMiddleware
}); // Extend store
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {}; // Execute listeners when state is changed
var listeners = plugin.get('onStateChange');
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
var _loop = function _loop() {
var listener = _step2.value;
store.subscribe(function () {
listener(store.getState());
});
};
for (var _iterator2 = (0, _getIterator2.default)(listeners), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
_loop();
} // Run sagas
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
sagas.forEach(sagaMiddleware.run); // Setup app
setupApp(app); // Run subscriptions
var unlisteners = {};
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = (0, _getIterator2.default)(this._models), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var _model = _step3.value;
if (_model.subscriptions) {
unlisteners[_model.namespace] = (0, _subscription.run)(_model.subscriptions, _model, app, onError);
}
} // Setup app.model and app.unmodel
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
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);
/**
* Create global reducer for redux.
*
* @returns {Object}
*/
function createReducer() {
return reducerEnhancer((0, _redux.combineReducers)((0, _objectSpread2.default)({}, reducers, extraReducers, app._store ? app._store.asyncReducers : {})));
}
}
首先我们来看第一行onError
,描述是全局错误处理,还记得Plugins
插件里面的onError
吗,没错,这里正是学习插件运行的最好机会呀,这个onError
肯定在报错的时候会调用的
// Global error handler
var onError = function onError(err, extension) {
if (err) {
if (typeof err === 'string') err = new Error(err);
err.preventDefault = function () {
err._dontReject = true;
};
plugin.apply('onError', function (err) {
throw new Error(err.stack || err);
})(err, app._store.dispatch, extension);
}
};
你看里面重要的plugin.apply
这里执行插件的onError
, 这个apply
可不是js原生的apply
来绑定this的呀,而是自己实现的方法,找到Plugins.js
插件下面的源码如下, 果然没错,把hooks
里面的onError
数组取出来叫做fns
, 然后返回一个函数return (...args)
的目的是为了获取实时调用的参数,然后遍历fns
分别传入参数一一调用,同时也支持传入一个默认的defaultHandler
,在没有注册插件的时候采用默认处理方式
apply(key, defaultHandler) {
const hooks = this.hooks;
const validApplyHooks = ['onError', 'onHmr'];
invariant(
validApplyHooks.indexOf(key) > -1,
`plugin.apply: hook ${key} cannot be applied`
);
const fns = hooks[key];
return (...args) => {
if (fns.length) {
for (const fn of fns) {
fn(...args);
}
} else if (defaultHandler) {
defaultHandler(...args);
}
};
}
这样一个插件的注册和调用的流程我们都理解了,是不是很像自己实现一套插件机制,这就是所有类库宣传的
可插拔
的高级特性,代码就这么回事,但是这种思想要建立起来,还有封装方式,这些都是宝贵的经验呢!!!
网友评论