If you have experience with Express, KOA, and Redux, they all have the concept of middlewares, which is the idea of an interceptor for adding some extra processing between a particular input and output without affecting the original operation.

I started out with middleware when I was using Express and KOA on the server side, and then I moved from the server side to the front end, and I saw it used in redux as well. Middleware design philosophy also brings flexibility and powerful extensibility to many frameworks.

This paper mainly compares the middleware implementation of Redux, KOA and Express. In order to be more intuitive, I will extract the core code related to the three middleware, simplify and write simulation examples. The express, KOA, and Redux examples will keep the overall structure of express, KOA, and Redux as consistent as possible with the source code, so this article will also explain the overall structure and key implementation of Express, KOA, and Redux:

Example source code address, you can read the source code, while reading the article, welcome star!

This article is for developers who have some experience with Express, KOA, and Redux

Server-side middleware

The middleware of Express and KOA is designed to handle HTTP requests and responses, but they are not designed in the same way. The middleware differences most people know about Express and KOA are:

  • expressWith “tail recursion”, the middleware executes sequentially, one after the other, accustomed to turningresponseThe response is written in the last middleware;
  • whilekoaMiddleware supportgeneratorThe order of execution is the onion ring model.

The so-called “Onion ring” model:

In fact, express middleware can also form an onion ring model, where code written after the next call will also be executed, but express doesn’t do this because express response is usually in the last middleware, Then other middleware code after next() has no effect on the final response result;

express

Let’s look at the express implementation first:

The entrance

// express.js

var proto = require('./application');
var mixin = require('merge-descriptors');

exports = module.exports = createApplication;

function createApplication() {// app is also a method, as the http.createserver handler var app =function(req, res, next) { 
      app.handle(req, res, next)
  }
  
  mixin(app, proto, false);
  return app
}

Copy the code

This is actually pretty simple, it’s just a createApplication method that creates an Express instance, and notice that the return value app is both an instance object that has a bunch of methods mounted on it, and it’s also a method itself, As a handler for http.createserver, the code is in application.js:

// application.js

var http = require('http');
var flatten = require('array-flatten');
var app = exports = module.exports = {}

app.listen = function listen() {
  var server = http.createServer(this)
  return server.listen.apply(server, arguments)
}

Copy the code

Here app.listen calls nodejs’s http.createServer to create a Web service. Var server = http.createserver (this) where this is the app itself and the actual handler is app.handle;

Middleware processing

Express is essentially a middleware manager that executes on the middleware when it enters app.handle, so the two most critical functions are:

  • App.handle tail recursively calls middleware to handle REQ and RES
  • App. use Adds middleware

Global maintenance of a stack array used to store all middleware, app. Use implementation is very simple, can be a line of code


// app.use
app.use = function(fn) {
	this.stack.push(fn)
}

Copy the code

Router, Route, and Layer are the three key classes. With router, you need to split path. Stack stores layer instances. The app.use method actually calls the router instance’s use method.

App.handle handles the stack array


app.handle = function(req, res, callback) {

	var stack = this.stack;
	var idx = 0;
	function next(err) {
		if (idx >= stack.length) {
		  callback('err') 
		  return;
		}
		var mid;
		while(idx < stack.length) {
		  mid = stack[idx++];
		  mid(req, res, next);
		}
	}
	next()
}

Copy the code

In what’s called a “tail recursive call”, the next method keeps calling the middleware function in the stack, passing next itself to the middleware as a third argument. The fixed form of each middleware convention is (req, RES, next) => {}, so that each “middleware” function simply calls the next method to pass the call to the next middleware.

Is said to be “tail recursion” because the last statement of recursive functions is to call the function itself, so the last statement of middleware in every need is next () to form the “tail recursion”, otherwise it is ordinary recursive, “tail recursion” compared with the ordinary “recursive” the advantage of saving memory space, not form a deeply nested function call stack. Those who are interested can read ruan’s last call optimization

At this point, the middleware implementation of Express is complete.

koa

I have to say that koA’s overall design and code implementation are more advanced and refined than Express’s. The code is based on ES6 implementation, supports generator(async await), there is no built-in routing implementation and any built-in middleware, the context design is also very clever.

As a whole

There are only four files:

  • Application. Js entry file, koA application instance class
  • context.js ctxInstance, proxy a lotrequestandresponseProperties and methods of the
  • request.js koaTo the nativereqObject encapsulation
  • response.js koaTo the nativeresObject encapsulation

Request.js and Response.js are nothing to say, and any Web framework will provide req and RES encapsulation to simplify processing. So let’s take a look at the implementation of context.js and application.js

// context.js 

/**
 * Response delegation.
 */

delegate(proto, 'res')
  .method('setHeader')

/**
 * Request delegation.
 */

delegate(proto, 'req')
  .access('url')
  .setter('href')
  .getter('ip');

Copy the code

Context is that kind of code, and the main thing it does is it acts as a proxy, using the delegate library.

Delegate (proto, ‘res’). Method (‘setHeader’) When proto.setHeader is called, proto.res.setHeader is called, that is, proto’s setHeader method is proto’s RES property, and so on.

// some of the code in application.jsconstructor() { super() this.middleware = [] this.context = Object.create(context) } use(fn) { this.middleware.push(fn) } listen(... args) { debug('listen')
	const server = http.createServer(this.callback());
	returnserver.listen(... args); }callbackConst fn = compose(this.middleware); const fn = compose(this.middleware); Const handleRequest = (req, res) => {const handleRequest = (req, res) => { Const CTX = this.createcontext (req, res); const CTX = this.createcontext (req, res);return this.handleRequest(ctx, fn);
	};
	
	return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
	ctx.statusCode = 404;
	const onerror = err => ctx.onerror(err);
	const handleResponse = () => respond(ctx);
	return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
   
Copy the code

Const server = http.createserver (this.callback()); Use this.callback() to generate a handler for the Web service

Callback returns handleRequest, so the real handler is this.handlerequest (CTX, fn).

Middleware processing

The constructor maintains the global middleware array this.middleware and a global instance of this.context (as well as request, Response, and other helper properties in the source code). Unlike Express, because there is no Router implementation, all this.middleware functions are generic “middleware” functions rather than complex Layer instances.

this.handleRequest(ctx, fn); Where CTX is the first parameter, Fn = compose(this.middleware) as the second argument, handleRequest will call fnMiddleware(CTX).then(handleResponse).catch(onError); So the key to intermediate handling is the compose method, which is a separate package called koa-compose. Take it out and have a look inside:

// compose.js

'use strict'

module.exports = compose

function compose (middleware) {

  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if(! fn)return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}


Copy the code

Is it like next in Express, except that it is in the form of promise, and is slightly more difficult to understand because it supports asynchrony: Each middleware is an async (CTX, next) => {}, which returns a promise after execution. The second parameter next is dispatch.bind(null, I + 1), which is used to pass the execution of the “middleware”. Each middleware is executed indirection. Resolve fails until the last middleware executes, which then executes the code after await next(), then resolve fails and continues until the first middleware fails, which finally causes the outermost promise resolve to fail.

The difference with Express is that koA’s response is handled not in the “middleware”, but after the middleware executes the returned Promise resolve:

return fnMiddleware(ctx).then(handleResponse).catch(onerror);

The response is processed at the end by handleResponse,” The middleware “sets ctx.body” and handleResponse mainly handles ctx.body, so koA’s “onion ring” model is valid and the code after await next() affects the final response.

At this point, the middleware implementation of KOA is complete.

redux

Have to say, redux design ideas and source code implementation is really beautiful, the overall code is not much, the Internet has been widely available redux source analysis, I will not go into details. Redux-middleware is a good example of middleware

This is one of the best documentation I’ve ever read, and it’s clearredux middlewareIs a beautiful interpretation of the evolution process fromTo analyze problemstoTo solve the problemAnd constantly optimize the thought process.

The overall

This article will focus on the middleware implementation of Redux. First, we will briefly describe the core processing logic of Redux. CreateStore is its entry program, factory method, and return a store instance. The most critical method for store instances is dispatch, which does one thing:

currentState = currentReducer(currentState, action)

Call Reducer, pass in the current state and return the new state with action.

So to simulate basic Redux execution, simply implement the createStore, Dispatch method. Other content, such as bindActionCreators, Combiner Creators, and Subscribe Monitor, is available as an ancillary app and can be ignored for now.

Middleware processing

Then comes the core “middleware” implementation, applymiddleware.js:

// applyMiddleware.js

import compose from './compose'

export default functionapplyMiddleware(... middlewares) {returncreateStore => (... args) => { const store = createStore(... args)let dispatch = () => {
      throw new Error(
        `Dispatching whileconstructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.` ) } const middlewareAPI = { getState: store.getState, dispatch: (... args) => dispatch(... args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch)return {
      ...store,
      dispatch
    }
  }
}

Copy the code

The extension provided by redux middleware is after the action is initiated and before it arrives at reducer. Its implementation idea is somewhat different from express and KOA. It does not add middleware handlers in front of it through encapsulation of Store. dispatch. Instead, they do this by recursively overwriting dispatches, passing the previous overwritten dispatch over and over again.

Each redux middleware is in the form of store => next => Action => {XXX}

There are two main levels of function nesting:

  • The outermost function receives the store argument, and the handler code in applymiddleware.js is const chain = middlewares.map(Middleware => Middleware (middlewareAPI)), The middlewareAPI is the incoming store. This layer is used to pass the Store API to middleware for use, mainly two apis:

    1. getState, direct transmissionstore.getState.
    2. dispatch: (... args) => dispatch(... args).The implementation here is pretty neat. It’s notstore.dispatchIs an external variabledispatch, this variable ends up pointing to the overriddendispatchAnd the reason for that is that forredux-thunkSuch asynchronous middleware calls internallystore.dispatchWhile still going through all the “middleware”.
  • The chain returned is the second level array, Each element of the array is a function next => Action => {XXX}, which can be understood as receiving one dispatch and returning one dispatch from the latter middleware.

  • Compose (f, g, h) returns () => f(g(h(.. args)))

Now understand the dispatch = compose(… Chain (store.dispatch) is relatively easy, the native store.dispatch passes in the last “middleware”, returns a new dispatch, and passes out to the previous middleware, Until the final dispatch is returned, when the overwritten dispatch is called, each “middleware” is executed from the outside in “onion ring” model.

At this point, the Redux middleware is complete.

Other Key points

It’s also worth learning about the implementation of the Redux middleware. In order for the middleware to be applied only once, applyMiddleware does not work on store instances, but on the createStore factory method. How do you understand that? So if applyMiddleware looks like this

(store, middlewares) => {}

ApplyMiddleware (Store, middlewares) is called multiple times to add the same middleware to the same instance. So the form of applyMiddleware is

(… middlewares) => (createStore) => createStore,

In this way, a new instance is created every time the middleware is applied, avoiding the problem of repeated application of middleware.

This form takes middlewares and returns a higher-order method of createStore, commonly known as the Enhance method of a createStore, which adds the use of middleware internally, You’ll notice that this method is in the same form as middleware layer 2 (Dispatch) => Dispatch, so it can also be used with Compose for multiple enhancements. CreateStore also has a third parameter, Enhance, for internal judgment. So middleware use of Redux can be written in two ways:

The first: CreateStore Store = applyMiddleware(middleware1, middleware2)(Reducer) initState)Copy the code
The second: CreateStore Receives an enhancer parameter to auto-enhance store = createStore(Reducer, initState, applyMiddleware(Middleware1, Middleware2))Copy the code

The second use makes the point straight and is more readable.

Throughout the implementation of Redux, functional programming is reflected incisively and vividly. The middleware form store => Next => Action => {xx} is a flexible embodiment of the function Coriolization. It can be used to fix store parameters in advance by converting multiple parameters into single parameters. Get a more explicit dispatch => dispatch, which makes Compose useful.

conclusion

In general, the implementation of Express and KOA is similar in that the next method is passed to make recursive calls, but KOA is in the form of a promise. Redux is slightly different from the other two in that it is first overwritten recursively outward, resulting in a recursively inward call at execution time.

To summarize three key similarities and differences (not just middleware) :

  1. Instance creation:expressUsing the factory method,koaIs the class
  2. koaImplementation of the syntax is more advanced, usingES6To supportgenerator(async await)
  3. koaNo built-inrouter, increasing thectxGlobal object, the overall code is more concise, more convenient to use.
  4. koaThe recursion of middleware ispromiseForm,expressusewhileLoops andnextTail recursion
  5. I preferreduxThe implementation of currie middleware form, more concise and flexible, functional programming is more obvious
  6. reduxdispatchOverwrite the way for middleware enhancement

Finally again attached simulation sample source code for learning reference, like welcome star, fork!

Answer a question

Express can also use async function as middleware for asynchronous processing. This is not possible because the middleware of Express executes a synchronous while loop. When the middleware contains both normal and async functions, the order of execution will be interrupted.


function a() {
  console.log('a')
}

async function b() {
  console.log('b')
  await 1
  console.log('c')
  await 2
  console.log('d')}function f() {
	a()
	b()
	console.log('f')}Copy the code

The output here is ‘a’ > ‘b’ > ‘f’ > ‘C’

If an async function is called directly from a normal function, the async function will execute synchronously to the code after the first await, and then immediately return a promise. Resolve will not be resolved until all async functions within the async function have completed asynchronously and the whole async function has completed.

Therefore, through the above analysis of express middleware implementation, if the async function is used as middleware, and the internal use of await to do asynchronous processing, then the following middleware will execute first, wait until the next call after await index will exceed! Express Async allows you to open comments and try them out for yourself.

2020-03-10 Problem correction

The simplified simulation of Express is wrong

while(idx < stack.length) {
  mid = stack[idx++];
  mid(req, res, next);
}
Copy the code

Has been corrected to


  mid = stack[idx++];
  mid(req, res, next);

Copy the code

So the experimental results of the last question are also wrong, and the conclusion is that express middleware can use Async as long as it is not used in an onion ring manner