Dva源码解读系列之app对象

433 阅读8分钟

(图片来自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 };