Anyone who has used Koa, Redux, or Express will be familiar with middleware, especially if you are exposed to the “Onion model” in the process of learning Koa. In this article, WE will learn the middleware of Koa, but we do not want to start with the well-known “onion model diagram”. Instead, we will introduce what middleware is in Koa.

Read our recent popular articles (thanks to digg’s encouragement and support 🌹🌹🌹) :

  • What can we learn from Axios project of 77.9K Star (895+ 👍)
  • Using these ideas and techniques, I have read many excellent open source projects (591+ 👍)
  • What I learned from 13K front-end open source projects (546+ 👍)

1. Koa middleware

In the index. D. compose header under @types/koa-compose we find the definition of the middleware type:

// @types/koa-compose/index.d.ts
declare namespace compose {
  type Middleware<T> = (context: T, next: Koa.Next) = > any;
  type ComposedMiddleware<T> = (context: T, next? : Koa.Next) = > Promise<void>;
}
  
// @types/koa/index.d.ts => Koa.Next
type Next = () = > Promise<any>;
Copy the code

By looking at the definition of the Middleware type, we can see that in Koa Middleware is a normal function that takes two parameters: context and next. Where context represents the context object and next represents a function object that returns a Promise object when called.

After understanding what Koa middleware is, let’s introduce the core of Koa middleware, namely the compose function:

function wait(ms) {
  return new Promise((resolve) = > setTimeout(resolve, ms || 1));
}

const arr = [];
const stack = [];

// type Middleware<T> = (context: T, next: Koa.Next) => any;
stack.push(async (context, next) => {
  arr.push(1);
  await wait(1);
  await next();
  await wait(1);
  arr.push(6);
});

stack.push(async (context, next) => {
  arr.push(2);
  await wait(1);
  await next();
  await wait(1);
  arr.push(5);
});

stack.push(async (context, next) => {
  arr.push(3);
  await wait(1);
  await next();
  await wait(1);
  arr.push(4);
});

await compose(stack)({});
Copy the code

Source of above code: github.com/koajs/compo…

For the above code, we want the value of the array arr to be [1, 2, 3, 4, 5, 6] after executing the compose(stack)({}) statement. We’re not going to worry about how the compose function is implemented here. Let’s analyze the execution process of the above three middleware if the array ARR is required to produce the desired results:

1. Start execution of the first middleware, push 1 into the ARR array, at which point the value of the ARR array is [1], and wait for 1 millisecond. To ensure that the first item of the ARR array is 2, we need to start executing the second middleware after calling the next function.

2. Start the second middleware, push 2 into the ARR array, at which point the arR array value is [1, 2], and continue to wait for 1 millisecond. To ensure that the second item of the ARR array is 3, we also need to start executing the third middleware after calling the next function.

3. Start execution of the third middleware, push 3 into the ARR array, at which point the arR array value is [1, 2, 3], and wait for another 1 millisecond. To ensure that the third item in the ARR array is 4, we need to be able to continue execution after calling the third intermediate next function.

4. When the third middleware is executed, the value of the ARR array is [1, 2, 3, 4]. Therefore, in order to ensure that the fourth item in the ARR array is 5, we need to return the second middleware next function after the third middleware execution.

5. When the second middleware is executed, the arR array is [1, 2, 3, 4, 5]. Similarly, to ensure that item 5 of the ARR array is 6, we need to return the next function of the first middleware after the execution of the second middleware.

6. When the first middleware is executed, the arR array is [1, 2, 3, 4, 5, 6].

In order to understand the above execution process more intuitively, we can treat each middleware as a big task, and then take the next function as the dividing point, and break each big task into three small tasks: beforeNext, Next and afterNext.

In the figure above, we start with the beforeNext task of middleware 1, and then complete the task scheduling of middleware by following the execution steps of the purple arrow. In this article, Po examines the implementation of the Axios interceptor from three aspects: task registration, task choreography, and task scheduling. Similarly, Abo will analyze the Koa middleware mechanism from the above three aspects.

1.1 Task Registration

In Koa, after we create the Koa application object, we register the middleware by calling the use method of that object:

const Koa = require('koa');
const app = new Koa();

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

The use method is simple to implement. In the lib/application.js file, we find its definition:

// lib/application.js
module.exports = class Application extends Emitter {  
  constructor(options) {
    super(a);// Omit part of the code
    this.middleware = [];
  }
  
 use(fn) {
   if (typeoffn ! = ='function') throw new TypeError('middleware must be a function! ');
   // Omit part of the code
   this.middleware.push(fn);
   return this; }}Copy the code

The fn parameter is type-checked inside the use method, and when it passes, the middleware to which the fn is pointing is stored in the Middleware array. The this object is also returned, enabling chained calls.

1.2 Task Scheduling

In this article, Apogo looks at the design model of the Axios interceptor and extracts the following general task processing model:

In this general-purpose model, Po performs task choreography by placing the front processor and the rear processor in front and behind the CoreWork tasks, respectively. For Koa’s middleware mechanism, task choreography is accomplished by placing the front processor and the post processor in front and behind the await next() statement, respectively.

// The middleware that counts the request processing time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
Copy the code

1.3 Task Scheduling

From the previous analysis, we already know that middleware registered using the app.use method is stored in an internal Middleware array. To schedule tasks, you need to constantly pull middleware out of the Middleware array. The scheduling algorithm of the middleware is encapsulated in the compose function under the KOA-compose package. The specific implementation of this function is as follows:

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public* /
function compose(middleware) {
  // Omit part of the code
  return function (context, next) {
    // last called middleware #
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      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

The compose function takes a parameter of type array and returns a new function. For example, we can analyze await compose(stack)({}); The execution of a statement.

1.3.1 dispatch (0)

As you can see from the figure above, when the next function is called inside the first middleware, it continues to call the Dispatch function with the value of I being 1.

1.3.2 dispatch (1)

As you can see from the figure above, when the next function is called inside the second middleware, the dispatch function is still called, and the parameter I has a value of 2.

1.3.3 dispatch (2)

As you can see from the figure above, when the next function is called inside the third middleware, the dispatch function is still called, and the parameter I has a value of 3.

1.3.4 dispatch (3)

As you can see from the figure above, once the middleware in the Middleware array has started to execute, if the next parameter is not explicitly set, the statement that returns the next function will continue to execute. When the execution of the third middleware is complete, the statement after the next function of the second middleware is returned and continues execution until all the statements defined in the middleware have been executed.

After analyzing the implementation code for the Compose function, let’s look at how Koa internally uses the Compose function to handle registered middleware.

const Koa = require('koa');
const app = new Koa();

/ / response
app.use(ctx= > {
  ctx.body = 'Hi, I'm Brother Po.';
});

app.listen(3000);
Copy the code

Using the above code, I can quickly start a server. We have already analyzed the use method, so let’s analyze the LISTEN method. The implementation of this method is as follows:

// lib/application.js
module.exports = class Application extends Emitter {  
  listen(. args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}
Copy the code

Obviously, inside the Listen method, the server is created by calling the node.js built-in HTTP module’s createServer method, and then it starts listening on the specified port, which means it starts waiting for the client to connect. In addition, when we call the http.createserver method to create the HTTP server, we pass in this.callback(), which is implemented as follows:

// lib/application.js
const compose = require('koa-compose');

module.exports = class Application extends Emitter {  
  callback() {
    const fn = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error'.this.onerror);

    const handleRequest = (req, res) = > {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    returnhandleRequest; }}Copy the code

Inside the callback method, we finally see the long-lost compose method. When the callback method is called, a handleRequest object is returned to handle the HTTP request. The handleRequest method is called every time the Koa server receives a client request, which first creates a new Context object and then executes the registered middleware to process the received HTTP request:

module.exports = class Application extends Emitter {  
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err= > ctx.onerror(err);
    const handleResponse = () = > respond(ctx);
    onFinished(res, onerror);
    returnfnMiddleware(ctx).then(handleResponse).catch(onerror); }}Copy the code

Ok, Koa middleware content has been basically introduced, interested in Koa kernel partners, you can research. Next, we will introduce the Onion model and its application.

Read other source code analysis articles and more than 50 “Re-learn TS” tutorials in full Stack.

Two, onion model

2.1 Introduction to onion model

(Photo credit: eggjs.org/en/intro/eg…

In the figure above, each layer in the onion represents a separate piece of middleware that implements different functions, such as exception handling, cache handling, and so on. Each request goes through each layer of middleware, starting on the left, and when it reaches the innermost middleware, it returns from the innermost middleware layer by layer. So for each layer of middleware, there are two points in a request and response cycle to add different processing logic.

2.2 Application of onion model

In addition to the application of the Onion model in Koa, the onion model has been widely used in some good projects on Github, such as KOA-Router and Alibaba’s Midway and UmI-Request projects.

After introducing Koa’s middleware and onion model, Abobo extracts the following general task processing model according to his own understanding:

The middleware described in the figure above is generally business-neutral generic functional code, such as middleware for setting response times:

// x-response-time
async function responseTime(ctx, next) {
  const start = new Date(a);await next();
  const ms = new Date() - start;
  ctx.set("X-Response-Time", ms + "ms");
}
Copy the code

Both front and rear processors are optional for each piece of middleware. For example, the following middleware is used to set up a unified response:

// response
async function respond(ctx, next) {
  await next();
  if ("/"! = ctx.url)return;
  ctx.body = "Hello World";
}
Copy the code

Although both of the middleware mentioned above are relatively simple, you can implement complex logic depending on your needs. Koa’s core is very light, but the sparrow is small. It provides an elegant middleware mechanism, so that developers can flexibly extend the function of the Web server, this design idea is worth learning and reference. Ok, I will stop here for this time. If there is a chance, I will introduce the middleware mechanism of Redux or Express separately.

3. Reference resources

  • Koa official document
  • Egg official document