5

Preface

Middleware (middleware) originally meant to refer to a general and independent system software service program that is located on the server's operating system and manages computing resources and network communications. Distributed application software uses this software to share resources between different technologies. In the big front-end field, the meaning of Middleware is much simpler, generally referring to data processing functions that provide general independent functions. Typical Middleware includes logging, data overlay, and error handling. This article will horizontally compare the Middleware usage scenarios and implementation principles of major frameworks in the big front-end field, including Express, Koa, Redux and Axios.

Middleware in the big front-end field

The big front-end field mentioned here naturally includes server-side and client-side. Express was the first to put forward the concept of Middleware, and then Koa, built by the original team, not only followed the architecture design of Middleware, but also more thoroughly defined itself as a middleware framework.

Expressive HTTP middleware framework for node.js

In the client field, Redux also introduced the concept of Middleware to facilitate independent functions to process Actions. Although Axios does not have middleware, the usage of its interceptor is very similar to that of middleware. The following table horizontally compares the use of middleware or class middleware of several frameworks.

frameuse registrationnext schedulingcompose orchestrationProcessing object
ExpressYYNreq & res
KoaYYYctx
ReduxNYYaction
AxiosYNNconfig/data

Let's disassemble the internal implementation of these frameworks together.

Express

usage

app.use(function logMethod(req, res, next) {
  console.log('Request Type:', req.method)
  next()
})

Express Middleware has multiple levels of registration methods. Here we take application-level middleware as an example. Here are two keywords, use and next. Express is registered through use and next triggers the execution of the next middleware, which establishes the standard usage of the middleware architecture.

principle

The principle part will be extremely streamlined to the source code, leaving only the core.

Middleware registration (use)

var stack = [];
function use(fn) {
  stack.push(fn);
}

Middleware scheduling (next)

function handle(req, res) {
  var idx = 0;
  next();
  function next() {
    var fn = stack[idx++];
    fn(req, res, next)
  }
}

When the request arrives, the handle method is triggered. Then the next function sequentially removes the Middleware from the queue and executes it.

Koa

usage

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

Compared with Express, Koa's Middleware registration has nothing to do with routing, and all requests will go through the registered middleware. At the same time, Koa inherently supports async/await asynchronous programming mode, and the code style is more concise. As for the onion model, everyone knows everything, so let's not talk nonsense.

principle

Middleware registration (use)

var middleware = [];
function use(fn) {
  middleware.push(fn);
}

Middleware Orchestration (koa-compose)

function compose (middleware) {
  returnfunction (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      index = i
      let fn = middleware[i]
      // middleware执行完的后续操作,结合koa的源码,这里的next=undefined
      if (i === middleware.length) fn = next
      if (!fn) returnPromise.resolve()
      try {
        returnPromise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        returnPromise.reject(err)
      }
    }
  }
}

Similar to Express, Koa's Middleware is also executed sequentially, controlled by the dispatch function. The code writing mode is also very similar: call dispatch/next -> define dispatch/next -> dispatch/next as a callback recursive call. There is one thing to note here. For Middleware, their await next() is actually await dispatch(i). When the execution reaches the last Middleware, the condition if (i === middleware.length) fn = next will be triggered, where next is undefined, the bar if (!fn) return Promise.resolve() will be triggered, and the execution will continue at the end The code behind a Middleware await next() is also the point in time when the onion model is executed from the inside out.

Redux

Redux is the first front-end framework I know of that applies the Middleware concept to the client. Its source code embodies the idea of functional programming everywhere, which is impressive.

usage

const logger = store =>next =>action => {
  console.info('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}
const crashReporter = store =>next =>action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
  }
}
const store = createStore(appReducer, applyMiddleware(logger, crashReporter))

The parameters of Redux middleware have been curried, store is passed in from within applyMiddleware, next is passed in after compose, and action is passed in by dispatch. The design here is indeed very clever, let's analyze it with the source code below.

principle

Middleware Orchestration (applyMiddleware)

export default function applyMiddleware(...middlewares) {
  return(createStore) =>(reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState)
    let dispatch = store.dispatch
    let chain = []
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    // 先执行一遍middleware,把第一个参数store传进去
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 传入原始的dispatch
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

Here the return value of compose is re-assigned to dispatch, indicating that the dispatch we call in the application is not the store's own, but an upgraded version processed by Middleware.

Middleware orchestration (compose)

function compose (...funcs) {
  if (funcs.length === 0) {
    returnarg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) =>(...args) => a(b(...args)))
}

The core code of compose is only one line, which nests the Middleware layer by layer like a doll. The bottom args is store.dispatch.

Axios

There is no concept of Middleware in Axios, but there are interceptors with similar functions, which are essentially between the two points of the data processing link, providing independent, configurable, and superimposable additional functions.

usage

// 请求拦截器
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});
// 响应拦截器
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});

Axios interceptors are divided into two types: request and response. After registration, they will be automatically executed in the order of registration, and there is no need to manually call next() like other frameworks.

principle

interceptors registration (use)

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
function InterceptorManager() {
  this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  returnthis.handlers.length - 1;
};

You can see that Axios maintains two interceptors internally, which have independent handlers arrays. Use is just adding elements to the array. The difference from other frameworks is that the array element here is not a function, but an object, containing two attributes fulfilled and rejected. When the second parameter is not passed, rejected is undefined.

Task scheduling

// 精简后的代码
Axios.prototype.request = function request(config) {
  config = mergeConfig(this.defaults, config);
  // 成对的添加元素
  var requestInterceptorChain = [];
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  
  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  var chain = [dispatchRequest, undefined];
  
  Array.prototype.unshift.apply(chain, requestInterceptorChain);
  chain.concat(responseInterceptorChain);
  
  promise = Promise.resolve(config);
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
}

Here through the chain call of promises, the interceptors are connected in series, and the order of execution is: requestInterceptorChain -> chain -> responseInterceptorChain. There is a default convention. The elements in the chain are arranged according to the pattern [fulfilled1, rejected1, fulfilled2, rejected2], so if the second parameter is not provided when registering interceptors, there will also be a default value of undefined.

Horizontal comparison of each frame

After looking at the implementation of Middleware of major frameworks, we can summarize the following characteristics:

  • Middleware mechanism can be used for both the server and the client
  • The Middleware mechanism is essentially to open one or more points on the data processing link to framework users to enhance the data processing capabilities of the framework
  • The vast majority of Middleware are reusable functions that do not depend on specific services
  • Multiple Middleware can be combined to achieve complex functions

Let's summarize the essence of the implementation of the middleware system of the major frameworks:

frameMethod to realize
ExpressCall next recursively
KoaRecursively call dispatch
ReduxArray.reduce implements function nesting
Axiospromise.then chain call

The most subtle and difficult to understand is the form of Array.reduce, which requires repeated scrutiny. The task scheduling method of promise.then chain call is also very clever. The data processed before will be automatically passed to the next then. The form of recursive calls is best understood. Koa naturally supports asynchronous calls based on the Express implementation, which is more in line with server-side scenarios.

to sum up

This article starts with the usage method, explains the implementation of Middleware in the major front-end frameworks in combination with the source code, and horizontally compares the similarities and differences between them. The techniques of recursive call, function nesting and promise chain call are very worthy of our learning.


兰俊秋雨
5.1k 声望3.5k 粉丝

基于大前端端技术的一些探索反思总结及讨论