preface

The status management tool with similar functions as DVA and NUOMI can be used in production environment

The overall structure is as follows:

  • Redux needless to say, a lot of hard to use, too much template code
  • Before we start to talk about packaging, we must understand the source code of Redux, I will take you from a white point of view, popular write a Redux out, especially if you do not understand the writing of middleware, you can not write extended middleware
  • This is because our code has its own processing middleware for asynchronous scenarios, and plugin mechanism is added, which is more reasonable than DVA. Currently, loading middleware is added, and each asynchronous request will have its own loading state, such as true before the request and false after the request
  • The admin tool is not coupled to purely enhanced Redux or anything like react-redux or react-router

A few words from redux Lite

What redux is all about, it’s very simple, we need a data warehouse, a store, we put the data in, we modify it, we create a new store. Let’s take an example

If: store is {count: 0}

Action (is just an object, such as {type: 'inc'.payload: 1}) ↓ Then process the data by putting store and action into the following functionsfunction reducer(store, action) {
    if(action,type === 'inc') {return { ...store, count: store.count + 1Reducer (store, action) ↓ and store updated to {count: 1 }
Copy the code

This is the core redux process, and it’s easy to understand.

So the simplest redux can be implemented this way

class redux{
    constructor(reducer, store){
        this.store = store;
        this.reducer = reducer;
    }
    dispatch(action){
        this.store = this.reducer(this.store, action);
    }
    getStore(){
        return this.store;
    }
    createStore(){
        return ({
            dispatch,
            getStore
        })
    }
}
Copy the code

Redux also has a little publish-subscribe feature, which we also added, publish-subscribe, which is basically a very simple implementation that we immediately understand, an array holds a function called publish-which is called subscribe

const listeners = [];
listeners.push(() = >{ console.log('I'm subscribed! ')})Copy the code

The code above is the publication of a publication subscription, publishing an event ()=>{console.log(‘ I’m subscribed! ‘)} waiting to be subscribed,

So subscribing is easy

for(let i = 0; i < listeners.length; i++){
    listeners[i]();
}
Copy the code

So we added publish-subscribe functionality to Redux along these lines:

class redux{
    constructor(reducer, initStore){
        this.store = initStore;
        this.reducer = reducer;
        this.listeners = [];
    }
    dispatch = (action) = > {
        this.store = this.reducer(this.store, action);
        for(let i = 0; i < this.listeners.length; i++){
            this.listeners[i]();
        }
    }
    getStore = () = > {
        return this.store;
    }
    subscribe = (listener) = > { 
        this.listeners.push(listener); 
    }
    createStore = () = > {
        return ({
            subscribe: this.subscribe,
            dispatch: this.dispatch,
            getStore: this.getStore
        })
    }
}
Copy the code

Well, you can try it:

const reducer = function(store, action) {
    if(action,type === 'inc') {return { ...store, count: store.count + 1}}return store;
}
const { subscribe, dispatch, getStore } = new redux(reducer, {count: 0}).createStore()

dispatch({ type: 'inc'.payload: 1 })

getStore() // { count: 1 }
Copy the code

Redux is a reduce-reducer reducer with a number of reduce-store reducers. The reducer reducer is a reduce-store reducer with a number of reduce-store reducers. 0}, we generally write this reducer

const reducer = function(store = { count: 0 }, action) {
    if(action? .type ==='inc') {return { ...store, count: store.count + 1}}return store;
}
Copy the code

If we let the reducer execute by default, is the store initialized for us? So we can do this.

class redux{
    constructor(reducer, initStore){
        this.store = store;
        this.reducer = reducer;
        this.listeners = [];
        this.dispatch({ type: Symbol() })
    }
    dispatch = (action) = > {
        this.store = this.reducer(this.store, action);
        for(let i = 0; i < this.listeners.length; i++){
            this.listeners[i](); }}... Other code}Copy the code

This. Dispatch ({type: Symbol()}) returns the reducer with no data and the store is the default.

All right, let’s test the code

class redux{
    constructor(reducer, initStore){
        this.store = initStore;
        this.reducer = reducer;
        this.listeners = [];
        this.dispatch({ type: Symbol() })
    }
    dispatch = (action) = > {
        this.store = this.reducer(this.store, action);
        for(let i = 0; i < this.listeners.length; i++){
            this.listeners[i]();
        }
    }
    getStore=() = > {
        return this.store;
    }
    subscribe = (listener) = > { 
        this.listeners.push(listener); 
    }
    createStore = () = > {
        return ({
            subscribe: this.subscribe,
            dispatch: this.dispatch,
            getStore: this.getStore
        })
    }
}
// Var is only used for browser debugging
var reducer = function(store = { count: 0 }, action) {
    if(action? .type ==='inc') {return { ...store, count: store.count + 1}}return store;
}

new redux(reducer).createStore().getStore(); // { count: 0 }
Copy the code

Var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var var

Suppose we merge two reducer1 and Reducer2 as follows:

var reducer1 = function(store = { count: 0 }, action) {
    if(action? .type ==='inc') {return { ...store, count: store.count + 1}}return store;
}

var reducer2 = function(store = { name: 'zs' }, action) {
    if(action? .type ==='changeName') {return { ...store, name: store.name }
    }
    return store;
}

var reducer = combineReducers({
    count: reducer1,
    person: reducer2
});

Copy the code

The principle is very simple, we have multiple reducer, as long as we call each reducer, whether we can find the corresponding actions, let’s look at the combineReducers implementation:

function combineReducers(reducers) {

  // reducerKeys = ['count', 'person']
  // Get each reducer key, because the store key also corresponds to it
  const reducerKeys = Object.keys(reducers)

  // New reducer after merge
  return function combination(store = {}, action) {
    / / the new store
    const nextstore = {}

    // Traverse all reducer to generate a new store
    for (let i = 0; i < reducerKeys.length; i++) {
      const key = reducerKeys[i]
      const reducer = reducers[key]
     
      The store corresponding to the reducer key is undefined when initialized
      const previousstore = store[key]
      
      // Put actions into each reducer to execute
      const nextstoreForKey = reducer(previousstore, action)
      
      // Get the new store
      nextstore[key] = nextstoreForKey
    }
    returnnextstore; }}Copy the code

Ok, let’s test it out:

new redux(reducer).createStore().getStore(); 
// { count: {count: 0}, person: {name: 'zs'} }
Copy the code

Finally, we worked out the middleware system, and the Redux middleware system was the most complex, even though it was only a few lines of code.

Let’s think about a problem: Store. dispatch is a synchronization function, and the dispatch directly triggers the change of store. How can we expand this function, such as printing time before dispatch and printing time after dispatch

Immediately you say, it is not simple, direct print is not just a matter of time, yes, you answer yes, but I still want to add some features, that is to say, can you have the print function of time as a plug-in, the whole dispatch functions into a chain, such as the action comes, call a plugin function first, then call plug-in b function. . And so on, finally calling the Dispatch function

The code below may be a little confusing

export default function compose(. funcs) {
  if (funcs.length === 1) {
    return funcs[0]}return funcs.reduce((a, b) = > (. args) = >a(b(... args))) }Copy the code

How do we write our plug-in function

// We write a log plug-in
const logger = (store) = > (next) = > (action) = >Next (action) Log handling after dispatch}Copy the code

If you write a redux plugin, the format must also be like this, with three arguments passed in at once.

  • The first one is store
  • The second is next, or Dispatch
  • The third one is action

It doesn’t matter if you don’t understand it, you can just skip it. It’s just for those who have the ability to study.

const { getStore, dispatch } = new redux(reducer).createStore();

getStore(); 
// { count: {count: 0}, person: {name: 'zs'} }

const logger1 = (store) = > (next) = > (action) = > {
    console.log('log1 start')
    next(action)
    console.log('log1 end')}const logger2 = (store) = > (next) = > (action) = > {
    console.log('log2 start')
    next(action)
    console.log('log2 end')}const store = getStore();
const middlewares = [logger1, logger2];
function compose(. funcs) {
  if (funcs.length === 1) {
    return funcs[0]}return funcs.reduce((a, b) = > (. args) = >a(b(... args))) }const chain = middlewares.map(middleware= > middleware(store));

letnewDispatch = compose(... chain)(dispatch);Copy the code

Testing:

NewDispatch ({type: 'a'}) // log1 start // log2 start // log2 end // log1 endCopy the code

Let’s get this straight. We were originally a function that wrapped dispatch like this

const logger1 = (dispatch) = > (action) = > {
    console.log('log1 start')
    dispatch(action)
    console.log('log1 end')}Copy the code

If we add another middleware, what do we do? Keep the dispatch(action) above.

const logger2 = (logger1) = > (action) = > {
    console.log('log1 start')
    logger1(action)
    console.log('log1 end')}Copy the code

Logger2 (Logger1 (Dispatch)) will do.

A better state management tool for Redux

Let’s see how we can use our tools. We register a Model and start it. Okay

// How to register
HbDva.addModel({
  Scope, the name of each module
  namespace: 'app'.// Initialization data for each module
  state: {
    count: 0
  },
  Reducer (redux) ¶
  reducers: {
    increment(state) {
      return { count: state.count + 1}},decrement(state) {
      return { count: state.count - 1}}},// Code that handles side effects, and the external dispatch call returns a promise
  // For example dispatch({type: 'app/incrementAsync'}).then(XXX)
  effects: {
    async incrementAsync(data, dispatch) {
      await new Promise((resolve) = > {
        setTimeout(() = > {
          resolve()
        }, 1000)
      })
      dispatch({ type: 'app/increment'}}}})// Start create store and register plugin
HbDva.start()
Copy the code

Usage:

// Asynchronous invocation
dispatch({ type: 'app/incrementAsync' }).then(xxx)

// Synchronous call
dispatch({ type: 'app/increment' })
Copy the code

The source code starts with createStore

  start() {
      // Register middleware, from our plugin registered middleware
      // The plugin mechanism is ignored here
    const middles = this.plugin
      .get<IMiddlewares>(MIDDLEWARES)
      .map((middleware) = > middleware({ effects: this.effects }));
      // Merge all addModels, that is, register the reducer of models
    const reducer = this.getReducer();
    // Merge middleware, implementation of asyncMiddeWare later
    this.store = applyMiddleware(... middles,this.asyncMiddeWare())(createStore)(reducer);
  }
Copy the code

Let’s first look at how to register model

class HbDva {
  // Store all models
  _models: IModel[];

  // Result of createStore
  store: Store | {};

  // Additional plugin to be loaded
  plugin: Plugin;
    
  // Save all the reducer side effects
  effects: IEffects;

  constructor(plugin: Plugin) {
    this._models = [];
    this.store = {};
    this.effects = {};
    this.plugin = plugin;
  }

  /** * How to register model *@param m model
   */
  addModel(model: IModel) {
    Add scope to reducer and effects of the model
    const prefixmodel = this.prefixResolve(model);
    this._models.push(prefixmodel); }}Copy the code

When addModel, we add namespace (scope) to reducer and effects names in all models. PrefixResolve is implemented as follows:

The key code is addPrefix, for example

  namespace: 'app',
  reducers: {
    increment(state) {
      return { count: state.count + 1}},decrement(state) {
      return { count: state.count - 1}}},Copy the code

To:

  reducers: {
    'app/increment'(state) {
      return { count: state.count + 1}},'app/decrement'(state) {
      return { count: state.count - 1}}},Copy the code

Effects also has a namespace. The source code is as follows:

  /** * prefixResolve Add scope */ to the reducer and effects of the model
  prefixResolve(model: IModel) {
    if (model.reducers) {
      model.reducers = this.addPrefix(model.reducers, model.namespace);
    }
    if (model.effects) {
      model.effects = this.addPrefix(model.effects, model.namespace);
    }
    this.addEffects(model.effects || {});
    return model;
  }

  addEffects(effects: IEffects) {
    for (const [key, value] of Object.entries(effects)) {
      this.effects[key] = value; }}addPrefix(obj: IModel['reducers'] | IEffects, namespace: string) {
    return Object.keys(obj).reduce((prev, next) = > {
      prev[`The ${namespace}/${next}`] = obj[next];
      return prev;
    }, {});
  }
Copy the code

Let’s look at how to merge reducer

  / * * * *@returns Merge all the reducer into a reducer */
  getReducer(): reducer {
    const reducers: IModel['reducers'] = {};
    for (const m of this._models) {
      // m is the configuration for each model
      reducers[m.namespace] = function (state: Record<string.any> = m.state, action: IAction) {
        // Organize the reducer of each module
        const everyreducers = m.reducers; // Reducers configuration object, which contains functions
        const reducer = everyreducers[action.type]; // Equivalent to the switch written earlier
        if (reducer) {
          return reducer(state, action);
        }
        return state;
      };
    }
    const extraReducers = this.plugin.get<IExtraReducers>(EXTRA_REDUCER);
    return combineReducers<Record<string.any> > ({... reducers, ... extraReducers, });Reducer1 :fn,reducer2:fn}
  }
Copy the code

Let’s see how asyncMiddleware works:

  asyncMiddeWare(): Middleware {
    return ({ dispatch, getState }) = > {
      return (next) = > async (action) => {
        if (typeof this.effects[action.type] === 'function') {
          return this.effects[action.type](action.data, dispatch, getState);
        }
        return next(action);
      };
    };
  }
Copy the code

The key code is

if (typeof this.effects[action.type] === 'function') {
          return this.effects[action.type](action.data, dispatch, getState);
        }
Copy the code

If I judge you to be Effects, I don’t just next(action), I call Effects, pass in the action information, and the native dispatch lets the user call it.

The Plugin mechanism

The source code is as follows:

import { IHooks, IPluginKey } from './definitions';
import { EXTRA_REDUCER, MIDDLEWARES } from './constants';

export default class Plugin {
  hooks: IHooks;

  constructor() {
    // initialize the hooks into arrays
    this.hooks = { [MIDDLEWARES]: [], [EXTRA_REDUCER]: [] };
  }

  use(plugin: Record<IPluginKey, any>) {
    // Push the function or object into the corresponding hook because it will be used multiple times
    const { hooks } = this;
    for (const key in plugin) {
      hooks[key].push(plugin[key as IPluginKey]);
    }
  }

  get<T extends keyof IHooks>(key: T): IHooks[T] {
    // Different hooks are handled differently
    if (key === EXTRA_REDUCER) {
      // Reduce all the objects into a total object, here only the object form can meet the following merge into Combine operation.
      return Object.assign({}, ... this.hooks[key]); }if (key === MIDDLEWARES) {
      return this.hooks[key]; // Other hooks return user-configured functions or objects}}}Copy the code

When we create a new plugin, we save a plugin’s data warehouse

Middlewares: [], // Store all extended middleware extraReducers: [], // store extra reducer}Copy the code

Let’s write a loadingPlugin like this:

import { Middleware } from 'redux';
import { HIDE, NAMESPACE, SHOW } from './constants';
import { reducer } from './definitions';

export default function createLoading() {
  const initalState: Record<string.any> = {
    effects: {}, // To collect effects true or false for each namespace
  };
  const extraReducers: { [NAMESPACE]: reducer } = {
    // Add the reducer to the combineReducer
    [NAMESPACE](state = initalState, { type, payload }) {
      const { actionType } = payload || {};
      switch (type) {
        case SHOW:
          return {
            effects: {
              ...state.effects,
              [actionType]: true,}};case HIDE:
          return {
            effects: {
              ...state.effects,
              [actionType]: false,}};default:
          returnstate; }}};const middleware: (. args:any[]) = > Middleware =
    ({ effects }) = >
    ({ dispatch }) = > {
      return (next) = > async (action) => {
        if (typeof effects[action.type] === 'function') {
          dispatch({ type: SHOW, payload: { actionType: action.type } });
          await next(action);
          dispatch({ type: HIDE, payload: { actionType: action.type } });
        }
        return next(action);
      };
    };
  return {
    middlewares: middleware,
    extraReducers,
  };
}

Copy the code

The key code

dispatch({ type: SHOW, payload: { actionType: action.type } });
Copy the code

Set the reducer state of loading to true, and you can fetch it from the store

 dispatch({ type: HIDE, payload: { actionType: action.type } });
Copy the code

This is the request back, and I set it to false

End of this article!