dva.js 源码理解

一. 先从官方快速上手 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 (
    
      
        
        
      
    
  );
}
export default RouterConfig;

dvarouter.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);
    }
  }

下面我们一步一步分解来看简单的分为以下几小步

  1. 前面一堆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.
  1. oldAppStart.call(app);这步其实很重要,因为首次进来肯定没有app._store的,这个oldAppStart其实是dva-core里面的start函数,用来调用reduxcreateStore, 放上源码,有点多啊,不想看怎么办,没事,我来给你细细讲解
  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);
      }
    };
  }

这样一个插件的注册和调用的流程我们都理解了,是不是很像自己实现一套插件机制,这就是所有类库宣传的可插拔的高级特性,代码就这么回事,但是这种思想要建立起来,还有封装方式,这些都是宝贵的经验呢!!!

你可能感兴趣的:(dva.js 源码理解)