[Source Code Learning--koa] Source Code Interpretation Analysis of Koa Middleware Core (koa-compose)

Recently, Koa has been used frequently for server-side development and has become fascinated with its onion model, which makes it great.And koa is mainly streamlined, there is not a lot of integration, everything needs to be loaded on demand, this is more to my taste.

In contrast to express's middleware, express's middleware uses concatenation, one after the other like icing sugar gourd, while koa uses a V-shaped structure (onion model), which gives our middleware a more flexible handling.

Based on the passion for onion models, this paper explores the onion model of koa. Both the middleware of koa1 and koa2 are based on koa-compose, which implements the V-type structure from koa-compose.
Attach source code first:

function compose (middleware) {
  //  The parameter middleware is an array of middleware that we useApp.use() One Middleware in series
  //  Determines if the list of middleware is an array and throws a type error if it is not
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  // Determines if the middleware is a function, if not, throws a type error
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
 1. @param {Object} context
 2. @return {Promise}
 3. @api public
   */
  
  return function (context, next) {
    // next here refers to the center function of the onion model
    // A context is a configuration object that holds some configurations and, of course, can be used to pass some parameters to the next intermediate
     
    // last called middleware #
    let index = -1  // Index is the index of the middleware that records execution
    return dispatch(0)  // Execute the first middleware and call the next one recursively through the first Middleware
    
    function dispatch (i) {
      // This is to ensure that a next() in the same middleware is not called multiple times 
      // When the next() function is called twice, i is less than index and an error is thrown
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i] // Remove the middleware to execute
      if (i === middleware.length) fn = next  // If i equals the length of the middleware, it goes to the center of the onion model (the last middleware)
      if (!fn) return Promise.resolve()  // If the middleware is empty, resolve directly
      try {
        //  Recursively execute the next middleware (focus on this below)
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

See here, if those below can understand, then the following can be ignored, or can not understand continue to look down, a bit more detailed analysis.

  1. First, we useApp.use() Add a middleware, in koa's source codeApp.use() This method is to push a middleware into the middleware list.It's written in the source code (this is easier to do without analysis):
 use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

The compose method passes into a list of middleware, which is the list of methods we added using use(). The first step is to determine if the list is an array, if the middleware is a method, or if it is not, throw a type error directly.

  1. compose returns a function that uses closures to cache a list of middleware, and then receives two parameters, the first being context, an object that holds configuration information.The second parameter is the next method, which is the center of the onion model or the inflection point of the V model.
  2. Create an index variable to hold the executed middleware index, then start recursive execution from the first middleware.
      let index = -1
      return dispatch(0)
  1. The dispatch method is to execute the middleware, first determine the index. If i is less than index, then the next function is executed twice or more in the same middleware. If i > index indicates that the middleware has not been executed, then record the reason for the middleware
        if (i <= index) return Promise.reject(new Error('next() called multiple times'))
        index = i
  1. Remove the middleware, if i equals the long image of the middleware, indicating that the center of the onion model was executed, then the last middleware, if it is empty, is directly resovle removed
        let fn = middleware[i]
        if(i === middleware.length){
          fn = next
        }
        if(!fn){
            return Promise.resolve()
        }
  1. When it comes to the most exciting part, it's also a bit around. First of all, what returns is a Promise object (Promise.resolve Is also a promise object, because when we await next(), await waits for and executes an async function to complete, and async returns a promise object by default, so here returns a promise object.In each middle, await mext() next() refers to the next middleware, that is
fn(context, function next () {
            return dispatch(i + 1)
          })

So await in our last one waited for dispatch(i+1) to complete, and dispatch returnedPromise.resolve(fn (context, function next () {XXXX}))), so although dispatch(0) was executed initially, it is this function that forms an execution chain.
In the case of three middleware executions, dispatch(0) is executed to form:

Promise.resolve( // First Middleware
  function(context,next){  // The second middleware of next here is dispatch(1)
     // Code on await next (middleware 1)
    await Promise.resolve( // Second Middleware
      function(context,next){  // The second middleware of next here is dispatch(2)
          // Code on await next (middleware 2)
        await Promise.resolve( // Third Middleware
          function(context,next){  // The second middleware of next here is dispatch(3)
             // Code on await next (middleware 3)
            await Promise.resolve()
            // Code under await next (middleware 3)
          }
        )
          // Code under await next (middleware 2)
      }
    )
      // Code under await next (middleware 2)
  }
) 

The code above await is executed first, then the last middleware resolve is passed up one by one, which forms an onion model.
Finally, attach the test code:

async function test1(ctx, next) {
    console.log('On Middleware 1');
    await next();
    console.log('Middleware 1');
  };
  
  async function test2(ctx, next) {
    console.log('On Middleware 2');
    await next();
    console.log('Middleware 2');
  };
  
  async function test3(ctx, next) {
    console.log('On Middleware 3');
    await next();
    console.log('Middleware 3');
  };
  let middleware = [test1, test2, test3];
  
  let cp = compose(middleware);
  
  cp('ctx', function() {
    console.log('core');
  });

OK, the koa2 middleware core (koa-compose) has been parsed here, and it has been around for a long time at first. See more times, analyze more step by step.The middleware of koa1 will wait a few days to fill in. Koa1 is based on generator and the source code is relatively simple compared to koa2.

Recently, I have been watching the koa2 source code, and wait until I have time to update the analysis of some of the koa source code.

Front-end pupils (graduates) who have errors or other ideas.Welcome to Orthogonal Stream~

Keywords: node.js less github

Added by darkke on Sun, 14 Jun 2020 19:21:23 +0300