This is the 25th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

I am small seventeen _, today with you read koA middleware source code, easy to understand, package education package will ~

  • IO /how-koa-mid…

Koa’s middleware is different from Express in that Koa uses the Onion model principle. Its source code contains only four files, very friendly for the students who read the source code for the first time. Today we will only look at the main file – application.js, which already contains the core logic of how the middleware works.

Lead to

First clone the KOA source code

git clone [email protected]:koajs/koa.git
npm install
Copy the code

We then add an index.js to the root of the project for testing

// index.js
// Include koA entry files
const Koa = require('./lib/application.js');
const app = new Koa();
const debug = require('debug') ('koa');
app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(6);
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// Time log
app.use(async (ctx, next) => {
  console.log(2);
  const start = Date.now();
  await next();
  console.log(5);
  const ms = Date.now() - start;
  ctx.set('X-Response-Time'.`${ms}ms`);
});
app.use(async (ctx, next) => {
  console.log(3);
  ctx.body = 'Hello World';
  await next();
  console.log(4);
});

app.listen(3000);
Copy the code

Run the following command to start the server:

node index.js
Copy the code

Then go to http://localhost:3000 and you’ll see 1, 2, 3, 4, 5, 6 output. This is called the Onion model

How the Onion model works

Let’s read koA’s core code to see how the middleware works. In index.js, we use middleware like this:

const app = new Koa();
app.use(// middleware);
app.use(// middleware);
app.listen(3000);
Copy the code

Let’s take a look at application.js, which is in the lib directory of the source code. Here is the middleware related code. I have simplified the code, keeping the core middleware logic and adding some comments to the code.

module.exports = class Application extends Emitter {
  
  constructor() {
    super(a);this.proxy = false;
    // Step 0: Initialize the middleware array
    this.middleware = [];
  }

  use(fn) {
    // Step 1: Push middleware into array
    this.middleware.push(fn);
    return this;
  }

  listen(. args) {
    debug('listen');
    // Step 2: Call this.callback() to combine all middleware
    const server = http.createServer(this.callback());
    returnserver.listen(... args); }callback() {
    // Step 3: The most important part of the compose function is the compose function
    // The middleware is combined into one big function that returns a promise, which we'll discuss later
    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);
    };

    return handleRequest;
  }

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err= > ctx.onerror(err);
    const handleResponse = () = > respond(ctx);
    onFinished(res, onerror);
    Step 4: Resolve this promise
    returnfnMiddleware(ctx).then(handleResponse).catch(onerror); }}Copy the code

Let’s simplify the code to pseudo-code for the compose function only:

 listen(. args) {
    const server = http.createServer(this.callback());
  }
  callback() {
    / / compose function
    const fn = compose(this.middleware);
    return this.handleRequest(ctx, fn);
  }
  handleRequest(ctx, fnMiddleware) {
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
Copy the code

The compose function, for example, returns a function called fn, which returns a promise.

About compose Function

For more information about the compose function, look at the source code for the KOa-compose package

module.exports = compose
function compose (middleware) {
  // Skip the type checking code here
  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

As we guessed above, the function that compose returns is called fn, and all middleware is passed by compose to the FN function, which returns dispatch(0), which immediately executes the dispatch function and returns a promise. Before we look at the contents of the Dispatch function, we need to look at the syntax of Promises.

On the Promise

We usually use promises like this:

const promise = new Promise(function(resolve, reject) {
    if (success){
        resolve(value);
    } else{ reject(error); }});Copy the code

In Koa, it is used like this:

let testPromise = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('test success');
  }, 1000);
});
Promise.resolve(testPromise).then(function (value) {
  console.log(value); // "test success"
});
Copy the code

Therefore, we know that in the compose function, it returns a promise.

Back to the KOa-compose middleware

module.exports = compose
function compose (middleware) {
  // Skip the type checking code here
  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

Dispatch is a recursive function that loops through all middleware. Let’s simplify the recursion:

let fn = middleware[i]
fn(context, dispatch.bind(null, i + 1))
Copy the code

Here fn is the current middleware function, which executes fn with context and dispatch.bind(null, I + 1) (which we passed to next), and the middleware executes this function, which recursively executes dispatch. Look at the following analysis:

In our test file index.js we have 3 middleware and all 3 middleware will execute this code before await next();

app.use(async (ctx, next) => {
    console.log(2);
    const start = Date.now();
    await next(); // <- stop here and wait for the next middleware to execute
    console.log(5);
    const ms = Date.now() - start;
    ctx.set('X-Response-Time'.`${ms}ms`);
});
Copy the code

We can look at the execution order of the three middleware in index.js:

  • performdispatch(0)When,Resolve (fn(context, dispatch.bind(null, 0 + 1)))
  • The first middleware content will run untilawait next()
  • next() = dispatch.bind(null, 0 + 1)This is the second middleware
  • The second middleware will run untilawait next()
  • next() = dispatch.bind(null, 1 + 1)This is the third middleware
  • The third middleware will run untilawait next()
  • next() = dispatch.bind(null, 2 + 1), no fourth middleware will pass immediatelyif (! fn) return Promise.resolve()Returns, in the third middlewareawait next()It is parsed and the remaining code in the third middleware is executed.
  • In the second middlewareawait next()The remaining code in the second middleware is executed.
  • In the first middlewareawait next()The remaining code in the first middleware is parsed and executed.

Why the Onion model?

If we had async/await in the middleware, coding would be easier. When we want to write a time logger for API requests, it is very easy to add this middleware:

app.use(async (ctx, next) => {
    const start = Date.now();
    await next(); // Your API logic
    const ms = Date.now() - start;
    console.log('API response time:' + ms);
});
Copy the code