preface

There are a lot of libraries on the front end, and the authors of these libraries try to cover as much as possible of the strange needs of everyone in the business, but there are always unexpected ones, so a good library needs to provide a mechanism for developers to intervene in the middle of the plug-in to fulfill their own needs.

This article will teach you how to write your own plug-in mechanism from implementations of KOA, Axios, Vuex, and Redux.

  • For starters: this article will help you figure out what mysterious plug-ins and interceptors really are.

  • For the veteran: Make your open source framework even more powerful by adding interceptors or plug-ins to it.

axios

First, we simulate a simple Axios that does not involve the logic of the request, but simply returns a Promise. The state of the Promise can be controlled with the error parameter in config.

Axios’s interceptor mechanism is represented by a flow chart that looks something like this:

const axios = config= > {
  if (config.error) {
    return Promise.reject({
      error: "error in axios"
    });
  } else {
    return Promise.resolve({ ... config,result: config.result }); }};Copy the code

If the config passed in has error, return a Rejected promise, and if it does not, return an Resolved promise.

Take a quick look at axios’s official interceptor example:

axios.interceptors.request.use(
  function(config) {
    // What to do before sending the request
    return config;
  },
  function(error) {
    // What to do about the request error
    return Promise.reject(error); });// Add a response interceptor
axios.interceptors.response.use(
  function(response) {
    // What to do with the response data
    return response;
  },
  function(error) {
    // Do something about the response error
    return Promise.reject(error); });Copy the code

As you can see, intercepting either request or response takes two functions as arguments, one to handle normal flow and one to handle failed flow. What does this suggest?

Yes, promise.then accepts these two parameters as well.

Internally, Axios takes advantage of the promise mechanism by putting two of the functions passed in by Use as an intercetpor, each of which has resolved and Rejected methods.

/ / the
axios.interceptors.response.use(func1, func2)

// Internal storage is
{
    resolved: func1,
    rejected: func2
}
Copy the code

Then a simple implementation, here we simplify, the axios. Interceptor. Request. Use to axios. UseRequestInterceptor to simple implementation:

// Construct an object to hold the interceptor
axios.interceptors = {
  request: [].response: []};// Register request interceptor
axios.useRequestInterceptor = (resolved, rejected) = > {
  axios.interceptors.request.push({ resolved, rejected });
};

// Register the response interceptor
axios.useResponseInterceptor = (resolved, rejected) = > {
  axios.interceptors.response.push({ resolved, rejected });
};

// Run interceptor
axios.run = config= > {
  const chain = [
    {
      resolved: axios,
      rejected: undefined}];// Push the request interceptor towards the array header
  axios.interceptors.request.forEach(interceptor= > {
    chain.unshift(interceptor);
  });

  // Push the response interceptor to the end of the array
  axios.interceptors.response.forEach(interceptor= > {
    chain.push(interceptor);
  });

  // Wrap config as a promise as well
  let promise = Promise.resolve(config);

  // violence while loop solves sorrow
  // Use promise.then's ability to recursively execute all interceptors
  while (chain.length) {
    const { resolved, rejected } = chain.shift();
    promise = promise.then(resolved, rejected);
  }

  // The last thing exposed to the user is the promise after the response interceptor has processed it
  return promise;
};
Copy the code

See the run-time mechanism from the axios.run function, first construct a chain as a promise chain, and construct the normal request (our request parameter axios) as an interceptor

  • Give the Request interceptor to unshiftchainAt the top of the
  • Give the Response interceptor to pushchainThe tail

Take this calling code as an example:

// Request interceptor 1
axios.useRequestInterceptor(resolved1, rejected1);
// Request interceptor 2
axios.useRequestInterceptor(resolved2, rejected2);
// Response interceptor 1
axios.useResponseInterceptor(resolved1, rejected1);
// Response interceptor
axios.useResponseInterceptor(resolved2, rejected2);
Copy the code

The resulting promise chain looks like this:

Request interceptor2./ / left configRequest interceptor1./ / left configAxios requests the core method,/ / left the responseResponse interceptor1./ / left the responseResponse interceptor/ / left the response
]
Copy the code

As for why the order of requestInterceptor is reversed, take a close look at the code to see what XD is.

Once you have the chain, all you need is one short line of code:

let promise = Promise.resolve(config);

while (chain.length) {
  const { resolved, rejected } = chain.shift();
  promise = promise.then(resolved, rejected);
}

return promise;
Copy the code

Promise will execute the chain from the top down.

Take this test code as an example:

axios.useRequestInterceptor(config= > {
  return {
    ...config,
    extraParams1: "extraParams1"
  };
});

axios.useRequestInterceptor(config= > {
  return {
    ...config,
    extraParams2: "extraParams2"
  };
});

axios.useResponseInterceptor(
  resp= > {
    const {
      extraParams1,
      extraParams2,
      result: { code, message }
    } = resp;
    return `${extraParams1} ${extraParams2} ${message}`;
  },
  error => {
    console.log("error", error); });Copy the code
  1. Successful call

Output result1: extraParams1 extraParams2 Message1 on a successful call

(async function() {
  const result = await axios.run({
    message: "message1"
  });
  console.log("result1: ", result); }) ();Copy the code
  1. Failed call
(async function() {
  const result = await axios.run({
    error: true
  });
  console.log("result3: ", result); }) ();Copy the code

On a failed call, the rejected branch of the response interceptor is entered:

First print the error log defined by the interceptor: error {error: ‘error in axios’}

Then due to the failure of the interceptor

error => {
  console.log('error', error)
},
Copy the code

Returns nothing, prints result3: undefined

It can be seen that axios’ interceptor is very flexible. It can arbitrarily modify config in the request stage and do various processing on response in the response stage. This is also because users’ demand for request data is very flexible and there is no need to interfere with users’ freedom.

vuex

Vuex provides an API to insert logic before and after an action is called:

Vuex.vuejs.org/zh/api/#sub…

store.subscribeAction({
  before: (action, state) = > {
    console.log(`before action ${action.type}`);
  },
  after: (action, state) = > {
    console.log(`after action ${action.type}`); }});Copy the code

It’s a bit like AOP (aspect oriented programming).

When store.dispatch({type: ‘add’}) is called, the log is printed before and after execution

before action add
add
after action add
Copy the code

Let’s make it simple:

import {
  Actions,
  ActionSubscribers,
  ActionSubscriber,
  ActionArguments
} from "./vuex.type";

class Vuex {
  state = {};

  action = {};

  _actionSubscribers = [];

  constructor({ state, action }) {
    this.state = state;
    this.action = action;
    this._actionSubscribers = [];
  }

  dispatch(action) {
    // action front listener
    this._actionSubscribers.forEach(sub= > sub.before(action, this.state));

    const { type, payload } = action;

    / / perform the action
    this.action[type](this.state, payload).then((a)= > {
      // action post-listener
      this._actionSubscribers.forEach(sub= > sub.after(action, this.state));
    });
  }

  subscribeAction(subscriber) {
    // Push listener into array
    this._actionSubscribers.push(subscriber); }}const store = new Vuex({
  state: {
    count: 0
  },
  action: {
    asyncadd(state, payload) { state.count += payload; }}}); store.subscribeAction({before: (action, state) = > {
    console.log(`before action ${action.type}, before count is ${state.count}`);
  },
  after: (action, state) = > {
    console.log(`after action ${action.type},  after count is ${state.count}`); }}); store.dispatch({type: "add".payload: 2
});
Copy the code

The console will print the following:

before action add, before count is 0
after action add, after count is 2
Copy the code

Easy to implement the logging function.

Of course, when Vuex implements plug-in functions, it selectively exposes type payload and state to the outside, and does not provide further modification capability. This is also a tradeoff within the framework. Of course, we can directly modify state. However, it will inevitably get warnings from Vuex, because in Vuex, all state changes should be made through mutations, but Vuex does not choose to expose commit, which also restricts the ability of the plug-in.

redux

To understand the middleware mechanism in Redux, you need to understand one method: compose

function compose(. funcs: Function[]) {
  return funcs.reduce((a, b) = >(... args: any) => a(b(... args))); }Copy the code

Compose (fn1, fn2, fn3) (swarm (fn1, fn2, fn3)) args) = > fn1(fn2(fn3(… Args))) is a higher-order aggregation function that executes FN3, passes the result to FN2, and passes the result to FN1.

With this prior knowledge, it’s easy to implement Redux’s middleware mechanisms.

Although the source code of Redux is written very little, various higher-order functions are various Corrified, but after the elaboration, the mechanism of Redux middleware can be explained in one sentence:

The dispatch method is repeatedly wrapped in higher-order functions and returns an enhanced Dispatch

For example, logMiddleware, which accepts a raw Redux dispatch, returns

const typeLogMiddleware = dispatch= > {
  // Returns a dispatch with the same structure and the same parameters
  // Just pack the original dispatch inside.
  return ({ type, ... args }) = > {
    console.log(`type is ${type}`);
    returndispatch({ type, ... args }); }; };Copy the code

With that in mind, let’s implement this Mini-Redux:

function compose(. funcs) {
  return funcs.reduce((a, b) = >(... args) => a(b(... args))); }function createStore(reducer, middlewares) {
  let currentState;

  function dispatch(action) {
    currentState = reducer(currentState, action);
  }

  function getState() {
    return currentState;
  }
  // Initialize an arbitrary dispatch that requires the external to return to its initial state if type does not match
  // After this dispatch currentState will have a value.
  dispatch({ type: "INIT" });

  let enhancedDispatch = dispatch;
  // If the second argument is passed into middlewares
  if (middlewares) {
    // Wrap middlewares into a function with compose
    // 让disenhancedDispatch = compose(... middlewares)(dispatch); }return {
    dispatch: enhancedDispatch,
    getState
  };
}
Copy the code

Then write two middleware pieces

/ / use

const otherDummyMiddleware = dispatch= > {
  // Return a new dispatch
  return action= > {
    console.log(`type in dummy is ${type}`);
    return dispatch(action);
  };
};

OtherDummyMiddleware returns otherDummyDispatch
const typeLogMiddleware = dispatch= > {
  // Return a new dispatch
  return ({ type, ... args }) = > {
    console.log(`type is ${type}`);
    returndispatch({ type, ... args }); }; };// Middleware executes from right to left.
const counterStore = createStore(counterReducer, [
  typeLogMiddleware,
  otherDummyMiddleware
]);

console.log(counterStore.getState().count);
counterStore.dispatch({ type: "add".payload: 2 });
console.log(counterStore.getState().count);

/ / output:
/ / 0
// type is add
// type in dummy is add
/ / 2
Copy the code

koa

Koa’s Onion model is a familiar one. This flexible middleware mechanism also makes KOA very powerful. This article will implement a simple Onion middleware mechanism as well. Reference (Middleware mechanism of UMi-Request)

Each ring in the onion is a piece of middleware that handles both incoming requests and response returns.

It and the middleware redux mechanism is similar, are of an essentially higher-order functions of nested, outer middleware layer of nested with middleware, this mechanism has the advantage of their ability to control the middleware (outer middleware can affect the inner request and response phase, the inner middleware can only affect the response of the outer phase)

So let’s first write Koa

class Koa {
  constructor() {
    this.middlewares = [];
  }
  use(middleware) {
    this.middlewares.push(middleware);
  }
  start({ req }) {
    const composed = composeMiddlewares(this.middlewares);
    const ctx = { req, res: undefined };
    returncomposed(ctx); }}Copy the code

Use is simply to push the middleware into the middleware queue. The core is how to compose the middleware, as follows:

function composeMiddlewares(middlewares) {
  return function wrapMiddlewares(ctx) {
    // Records the subscript of the middleware currently running
    let index = - 1;
    function dispatch(i) {
      // index moves backwards
      index = i;

      // Find the appropriate middleware in the array
      const fn = middlewares[i];

      // The last middleware call to next will not return an error
      if(! fn) {return Promise.resolve();
      }

      return Promise.resolve(
        fn(
          // Continue passing CTX
          ctx,
          // Next method, which allows access to the next middleware.
          () => dispatch(i + 1))); }// Start running the first middleware
    return dispatch(0);
  };
}
Copy the code

In short, dispatch(n) corresponds to the execution of the NTH middleware, and Dispatch (n) has the right to execute Dispatch (n + 1).

So when it actually runs, the middleware is not running flat, but nested higher-order functions:

Dispatch (0) contains Dispatch (1), and dispatch(1) contains Dispatch (2). In this mode, it is easy to think of the try catch mechanism, which can catch all errors of functions and functions that continue to be called within the function.

So our first middleware can do an error-handling middleware:

// The outermost layer controls a global error
app.use(async (ctx, next) => {
  try {
    // Next contains layer 2 and layer 3 runs
    await next();
  } catch (error) {
    console.log(`[koa error]: ${error.message}`); }});Copy the code

In this error-handling middleware, we run next wrapped in a try catch and call next to the second layer of middleware:

// Layer 2 logging middleware
app.use(async (ctx, next) => {
  const { req } = ctx;
  console.log(`req is The ${JSON.stringify(req)}`);
  await next();
  // Next will be able to get the third layer of data written into CTX
  console.log(`res is The ${JSON.stringify(ctx.res)}`);
});
Copy the code

After the next call from layer 2 middleware, layer 3, business logic processing middleware, is entered

// Layer 3 core service middleware
// In a real scenario this layer is used to construct the data that really needs to be returned and written to CTX
app.use(async (ctx, next) => {
  const { req } = ctx;
  console.log(`calculating the res of ${req}. `);
  const res = {
    code: 200.result: `req ${req} success`
  };
  / / write CTX
  ctx.res = res;
  await next();
});
Copy the code

After res is written to CTX in this layer, the function goes off the stack and goes back to await next() in the second layer middleware

console.log(`req is The ${JSON.stringify(req)}`);
await next();
// <- back here
console.log(`res is The ${JSON.stringify(ctx.res)}`);
Copy the code

At this point, the logging middleware can get the ctx.res value.

If you want to test the error-handling middleware, add it at the end

// Used to test global error middleware
// Comment this out for the middleware service to respond properly
app.use(async (ctx, next) => {
  throw new Error("oops! error!");
});
Copy the code

Finally, the start function is called:

app.start({ req: "ssh" });
Copy the code

The console prints the result:

req is "ssh"
calculating the res of ssh...
res is {"code":200."result":"req ssh success"}
Copy the code

conclusion

  1. axiosConstruct each interceptor that the user registers as a promise.then argument, and execute all interceptors as a promise chain at run time.
  • The config is already the result of the request interceptor’s processing before it is sent to the server
  • After the server responds to the result, the response passes through the response interceptor, and finally the user gets the processed result.
  1. vuexThe simplest implementation of Vuex is to provide two callback functions that can be called internally at the right time (I personally feel that most libraries provide this mechanism as well).
  2. reduxIn the source code of the most complex and most convoluted, its middleware mechanism is essentially the use of higher-order functions to continuously package and re-package the Dispatch, the formation of nesting dolls. This article has been simplified n times after the results, but the complex implementation is also for a lot of trade-offs and considerations, Dan for the use of closures and higher-order functions have been perfected, but outsiders to see the source code has nods…
  3. koaThe Onion model is well implemented and has similarities to Redux, but is personally superior to Redux’s middleware in terms of source code understanding and usage.

Middleware mechanisms are non-framework dependent. Request libraries can also be added to KOA’s Onion middleware mechanisms (such as UMi-Request). Different frameworks may be suitable for different middleware mechanisms, depending on what problem you are writing the framework to solve and what freedom you want to give users.

Hopefully, after reading this article, you will have a better understanding of the middleware mechanisms in front end libraries so that you can add appropriate middleware capabilities to your own front end libraries.

The code written in this article is organized in this repository: github.com/sl1673495/t…

The code is prepared using TS, js version of the code in the JS folder, you can see according to their own needs.