Dva is the react based secondary encapsulation, which is often used in work, so it is necessary to understand the implementation. This paper will analyze how DVA encapsulates Redux, Redux-Saga and React-Router from the source level.

The DVA version used in this article is 2.6.0-beta.20, for reference

  • Dva project address
  • Dva website

Dva build process

Here is a simple DVA application

// Initialize application const app = dva({onError(error) {console.log(error)}}) app.use(createLoading()) // use plugin app.model(model) // Router (app) // Router (app)'#app') // The render application copies the codeCopy the code

We’ll start with these apis to see how DVA encapsulates react applications.

The whole DVA project is managed by Lerna. Find the corresponding entry file of the module in package.json of each package, and then check the corresponding source code.

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;returnapp; // To expose the router interface for app, mainly update app._routerfunctionrouter(router) { app._router = router; } // For the app exposed start interface, mainly render the entire applicationfunctionStart (container) {// Find the corresponding DOM node based on the containerif(! app._store) oldAppStart.call(app); Start const store = app._store; 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 {
      returngetProvider(store, this, this._router); }}} // Render logicfunctionGetProvider (store, app, router) {const DvaRoot = extraProps => (// router.historyProp <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); } Duplicate codeCopy the code

As you can see, the app is initialized with create(opts, createOpts), where OPts is the configuration exposed to the user and createOpts is the configuration exposed to the developer. The actual create method is implemented in DVA-core.

dva-core craete

Here is a rough implementation of the create method

// dva-core/src/index.js
const dvaModel = {
  namespace: '@@dva',
  state: 0,
  reducers: {
    UPDATE(state) {
      returnstate + 1; ,}}};export functioncreate(hooksAndOpts = {}, createOpts = {}) { const { initialReducer, setupApp = noop } = createOpts; Const plugin = new plugin (); // createOpts const plugin = new plugin (); Plugin.use (filterHooks(hooksAndOpts)); Const app = {_models: [prefixNamespace({...dvaModel})], _store: null, _plugin: plugin, use: Plugin.use.bind (plugin), // Expose the use method to make it easy to write custom plug-in model, // expose the model method to register model start, // original start method, } via oldStart when applying render to DOM nodes;returnapp; } Duplicate codeCopy the code

As you can see, the code above mainly exposes the use, Model, and start interfaces.

Plug-in system

Dva implementation is highly dependent on the built-in Plugin system, so for the sake of understanding the following code, we will temporarily jump out of the start method and explore the Plugin principle.

A plugin for DVA is a JS object that contains some of the following properties, each corresponding to a function or configuration item

// The plugin object only exposes the following properties. As the name implies, methods starting with on fire at certain points throughout the application, similar to the lifecycle hook function const hooks = ['onError'.'onStateChange'.'onAction'.'onHmr'.'onReducer'.'onEffect'.'extraReducers'.'extraEnhancers'.'_handleActions',]; Copy the codeCopy the code

A rough plug-in structure looks something like this

const testPlugin = { onStateChange(newState){ console.log(newState) } // ... Hook} app.use(testPlugin) copies the codeCopy the code

Take a look at the Plugin manager constructed at start initialization

// Plugin managed object class Plugin {constructor() { this._handleActions = null; // Each hook handler defaults to an empty array this.hooks = cross.reduce ((memo, key) => {memo[key] = [];returnmemo; }, {}); Use (plugin) {const {hooks} = this;for (const key in plugin) {
      if(Object.prototype.hasOwnProperty.call(plugin, key)) { // ... Detects keys on the Plugin for use and handles specific forms of keysif (key === '_handleActions') { this._handleActions = plugin[key]; // If the Plugin's _handleActions method is defined, the Plugin object's _handleActions} will be overridden.else if (key === 'extraEnhancers') { hooks[key] = plugin[key]; // If the plug-in's extraEnhancers method is defined, this. Hooks' own extraEnhancers} is overridden.else{ hooks[key].push(plugin[key]); }}}} // Returns a function apply(key, which executes all methods registered on hooks via key). defaultHandler) { const { hooks } = this; const fns = hooks[key]; // Iterate over FNS and execute thisreturn(... args) => {}; } // Get (key) {const {hooks} = this;if (key === 'extraReducers'[key]) {/ / the hooks are merged into an object, such as [{x: 1}, {2} y:] into 2} {x: 1, y: objectreturn getExtraReducers(hooks[key]);
    } else if (key === 'onReducer') {// Returns a function that accepts a reducer and runs sequels of autos. onReducer, each loop taking the value returned by the previous autos. onReducer element method as the new reducerreturn getOnReducer(hooks[key]);
    } else {
      returnhooks[key]; }}}function getOnReducer(hook) {
  return function(reducer) {
    for (const reducerEnhancer of hook) {
      reducer = reducerEnhancer(reducer);
    }
    returnreducer; }; } Duplicate codeCopy the code

It can be seen that the implementation of the whole plug-in system is relatively simple, mainly exposing the use registration plug-in, get according to the key to obtain plug-in interface.

App.use = plugin.use.bind(plugin) Copies the codeCopy the code

Initialize the Store

Returning to the previous section and continuing to look at the execution flow of the Start method, we can see the completed Store build process

/ / oldStart methodfunction start() {// The global error handler, if a plugin registers the onError hook, will be used here via plugin.apply().'onError') call const onError = (err, extension) => {}; // Iterate over app._models, collect 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)); }} // Create app._store, see createStore // app._store = craeteStore() in the following section... const store = app._store; // Extend store store.runSaga = sagaMiddleware.run; store.asyncReducers = {}; // listeners when state is changed Subscribe to redux const listeners = plugin.get('onStateChange');
  for(const listener of listeners) { store.subscribe(() => { listener(store.getState()); }); } // Run all sagas sagas.foreach (sagamiddleware.run); setupApp(app); // Run all subscriptions to model methods const unlisteners = {};for (const model of this._models) {
    if(model.subscriptions) { unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError); } // Register the model, unmodel, and replaceModel interfaces when executing start, as described in the following section. } Duplicate codeCopy the code

Visible throughout the start method

  • traverseapp._models, collect and runsagas.
  • Initialize theapp._store, the use ofstore.subscribeRegister all plug-insonStateChangeThe callback
  • Run allmodel.subscriptions
  • exposedapp.model,app.unmodel,app.replaceModelThree interfaces

createStore

The createStore method is a encapsulated Redux createStore, passed in some middleware and reducers

const sagaMiddleware = createSagaMiddleware(); // Initialize saga middleware 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
  returncreateStore(reducers, initialState, compose(... enhancers)); } Duplicate codeCopy the code

The reducers parameter is constructed by the createReducer method, which returns an enhanced reducer

const reducerEnhancer = plugin.get('onReducer'); Const extraReducers = plugin.get()'extraReducers'); // Passing extraReducers actually returns the combined reducerObj objects of all the plug-insfunction createReducer() {
    returnreducerEnhancer( // redux.combineReducers combineReducers({ ... Reducers, // start default reducer... extraReducers, ... (app._store ? app._store.asyncReducers : {}), }), ); } Duplicate codeCopy the code

PromiseMiddleware is created by createPromiseMiddleware, a middleware that returns promises when isEffect(action.type) is used. And mount the Promise’s {__dvA_resolve: resolve, __dva_reject: reject} on the original action.

export default function createPromiseMiddleware(app) {
  return () => next => action => {
    const { type} = action; // isEffect is implemented as: find namespace from app._models and action.type.split('/')[0] Same model, then according to model. Effects [type] Exists to determine whether the current action needs to return a Promiseif (isEffect(type)) {
      returnnew Promise((resolve, reject) => { next({ __dva_resolve: resolve, __dva_reject: reject, ... action, }); }); }else {
      returnnext(action); }}; } Duplicate codeCopy the code

The model of three interfaces

A model is roughly the following structure

{
    namespace: 'index_model',
    state: {
        text: 'hello'
    },
    effects: {
        * asyncGetInfo({payload = {}}, {call, put}) {
        }
    },
    reducers: {
        updateData: (state, {payload}) => {
            return{... state, ... Payload}}}} Copy codeCopy the code

In the create method, the app.model method is implemented as follows, essentially

functionModel (m) {// Process the reducers and effects keys on model as'${namespace}${NAMESPACE_SEP}${key}Const prefixedModel = prefixNamespace({... m }); app._models.push(prefixedModel);returnprefixedModel; } Duplicate codeCopy the code

After executing the start method, the app.model method is overwritten as injectModel and unmodel and replaceModel are added

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); // Add the model dynamically so that the model can be loaded asynchronously similar to the Route componentfunctioninjectModel(createReducer, onError, unlisteners, m) { m = model(m); const store = app._store; store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions); // call createReducer store.replacereducer (createReducer());if (m.effects) {
    store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts)); } // Unlisteners are updated for subsequent removalif(m.subscriptions) { unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError); }} // Remove the model corresponding to the namespace. Remove store.asyncreducers and call Store.replacereducer (createReducer()) to regenerate the reducerfunctionUnmodel (createReducer, reducers, unlisteners, Namespace) {} // Find models with the same namespace and replace them. If they don't exist, add them directlyfunctionReplaceModel (createReducer, Reducers, unlisteners, onError, m) {} Duplicate codeCopy the code

According to this, models in DVA are divided into two categories

  • The model registered before calling start
  • Dynamically registered model after a call to start

routing

In the previous dvA. start method we saw createOpts and saw that the corresponding method was called at different times in the start of DVA-core

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); }}; Copy the codeCopy the code

Where initialReducer and setupMiddlewares are called when the store is initialized before setupApp is called

ConnectRouter and routerMiddleware both use the Connect-React-Router library. The main idea is that route hops are also treated as a special action.

Let’s first look at the implementation of the connectRouter method, which returns a Reducer about the router

// connected-react-router/src/reducer.js
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'const createConnectRouter = (structure) => { const { fromJS, Merge} = Structure // Two tool methods for copying and merging JS objects 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, isFirdering} = payload // State is not updated for the first renderingreturn isFirstRendering
          ? state
          : merge(state, { location: fromJS(injectQuery(location)), action })
      }
      return state
    }
  }

  returnCreateRouterReducer} Copy codeCopy the code

Then there’s routerMiddleware, which returns middleware about the Router

export const CALL_HISTORY_METHOD = '@@router/CALL_HISTORY_METHOD'
const routerMiddleware = (history) => store => next => action => {
  if(action.type ! == CALL_HISTORY_METHOD) {returnNext (action)} // called when action is a route jumphistoryNext const {payload: {method, args}} = actionhistory[method](... Args)} copy the codeCopy the code

Finally, go back to the DVA and look at the patchHistory method

function patchHistory(history) {// hijacked the history.listen method const oldListen = history.listen; history.listen = callback => { const cbStr = callback.toString(); / / when using dva. RouterRedux. ConnectedRouter routing component, its constructor executes history. Listen, isConnectedRouterHandler will be at this timetrue
    const isConnectedRouterHandler =
      (callback.name === 'handleLocationChange' && cbStr.indexOf('onLocationChanged') > -1) ||
      (cbStr.indexOf('.inTimeTravelling') > -1 && cbStr.indexOf('arguments[2]') > 1); // Used in other places after app.start such as Model.SubscriptionshistoryCallback (history.location, history.action);return oldListen.call(history, (... args) => {if(isConnectedRouterHandler) { callback(... args); }else {
        // Delay all listeners besides ConnectedRouter
        setTimeout(() => {
          callback(...args);
        });
      }
    });
  };
  return history; } Duplicate codeCopy the code

As you can see, DVA does not encapsulate the router too much, but provides a reducer and a middleware through connected- React-router, and exposes the library as routerRedux.

summary

As can be seen from the source code, DVA mainly encapsulates Redux and Redux-Saga, simplifying and exposing a few limited interfaces. The React-Router and other libraries are built in, making it a lightweight application framework

  • dvaThe API is mainly provided externally
  • dva-corerightreduxandredux-sagaA simple plug-in system is implemented

One of the benefits of learning the DVA source code is that it allows you to see the whole React ecosystem and learn how to use a decent development solution. Understanding the encapsulation implementation of DVA is only the first step. In order to write efficient and maintainable code, we need to go deep into the use and implementation of libraries such as Redux, Redux-Saga and React-Router.