koa intermediate implementation mechanism

start

Based on koa 2.11, it is analyzed according to the following process:

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

const one = (ctx, next) => {
  console.log('1-Start');
  next();
  ctx.body = { text: 'one' };
  console.log('1-End');
}
const two = (ctx, next) => {
  console.log('2-Start');
  next();
  ctx.body = { text: 'two' };
  console.log('2-End');
}

const three = (ctx, next) => {
  console.log('3-Start');
  ctx.body = { text: 'three' };
  next();
  console.log('3-End');
}

app.use(one);
app.use(two);
app.use(three);

app.listen(3000);

app.use()

The use method is defined in koa/lib/application.js:

use(fn) {
  // check middleware type, must be a function
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  // Compatible with generator
  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 || '-');
  
  // Storage center
  this.middleware.push(fn);
  return this;
}

this.middleware

This is an array that holds all the middleware and executes in order.

this.middleware = [];

app.listen()

This method is defined in koa/lib/application.js:

listen(...args) {
  debug('listen');
  
  // Create http service and listen
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

this.callback()

callback() {
  // Processing middleware
  const fn = compose(this.middleware);

  if (!this.listenerCount('error')) this.on('error', this.onerror);

  const handleRequest = (req, res) => {
    // Create Context
    const ctx = this.createContext(req, res);
    // Execute middleware to process requests and responses
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}

this.handleRequest

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  // Function that will respond to the issue
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  // Here, ctx will be transferred to middleware for processing,
  // When the middleware process is completed,
  // Will execute then function to send out the response
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

respond(ctx)

function respond(ctx) {
  // Omit other codes
  // ...
  // Send out response
  res.end(body);
}

As can be seen from the above code, the array in the middle is processed by the compose method, then a fnMiddleware function is returned, and the Context is passed to this function for processing. After the fnMiddleware is executed, the response method is used to send the response.

compose(this.middleware)

The compose function is introduced through koa compose

const compose = require('koa-compose');

Compose is defined under koajs/compose/index.js

function compose (middleware) {
  // The passed in must be an array
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  // Array must be a function
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function (context, next) {
    // This index indicates the number of middleware executed last time
    let index = -1
    
    // Execute the first Middleware
    return dispatch(0)
    function dispatch (i) {
      // Check whether the middleware has been executed,
      // For example, when the first middleware is executed, dispatch(0),
      // i = 0, index = -1, indicating no execution,
      // Then index = i, and index is saved through a closure,
      // If it is executed multiple times, an error will be reported
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      
      // Get the middleware from the array through the incoming index
      let fn = middleware[i]
      
      // If the current index is equal to the length of the middleware array,
      // It indicates that the middleware has been executed,
      // There is no second parameter passed in when fn is fnMiddleware(ctx),
      // That is fn = undefined
      if (i === middleware.length) fn = next
      // fn is undefined, return a promise that has been resolved
      if (!fn) return Promise.resolve()
      
      try {
        // Execute middleware functions and pass in dispatch as next function
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

End execution process

Now let's take a look at the execution process of fnMiddleware:

// fnMiddleware receives two parameters
function (context, next) {
  // ....
}

// Pass in context, not next,
// So the next is not passed in the first time
fnMiddleware(ctx).then(handleResponse).catch(onerror);

When next == undefined, the execution of middleware will be ended. The process is as follows:

function dispatch (i) {
  //...
  
  // Get the middleware from the array through the incoming index,
  // But because all middleware has been executed,
  // So the current i is equal to the array length,
  // That is fn = undefined
  let fn = middleware[i]

  // If the current index is equal to the length of the middleware array,
  // It indicates that the middleware has been executed,
  // Because fnMiddleware(ctx) does not have the second parameter next passed in,
  // So fn = undefined
  if (i === middleware.length) fn = next
  
  // fn is undefined, return a promise that has been resolved
  // End of middleware execution process
  if (!fn) return Promise.resolve()
  
  // ...
}

Middleware execution process

First, we talked about the end process. Now, we talk about how to execute in order to form an onion model:

function dispatch (i) {
  // ... omit other codes
  
    try {
    // Step by step instructions
    // First, build the dispatch as the next function through bind
    const next = dispatch.bind(null, i + 1);
    // Transfer CTX and next to execute the current middleware,
    // When you call next() in a middleware,
    // In essence, it calls diapatch(i + 1),
    // That is to get the next middleware from the array for execution,
    // At this time, the execution process of the current middleware will be interrupted and the next middleware will be executed,
    // Only after the execution of the next middleware is completed will the execution of the current middleware be resumed
    const result = fn(context, next);
    // After middleware execution, return the resolve d promise,
    // The last middleware then performs the rest of the process,
    // And that's what makes the onion model
    return Promise.resolve(result);
  } catch (err) {
    return Promise.reject(err)
  }
}

The execution result of the example at the beginning is as follows:

const one = (ctx, next) => {
  console.log('1-Start');
  next();
  ctx.body = { text: 'one' };
  console.log('1-End');
}
const two = (ctx, next) => {
  console.log('2-Start');
  next();
  ctx.body = { text: 'two' };
  console.log('2-End');
}

const three = (ctx, next) => {
  console.log('3-Start');
  ctx.body = { text: 'three' };
  next();
  console.log('3-End');
}

// 1-Start
// 2-Start
// 3-Start
// 3-End
// 2-End
// 1-End
// And ctx.body ends up with {text: 'one'}

next()

No call to next()

// No next() function called
app.use((ctx, next) => {
  console.log('Start');
  ctx.body = { text: 'test' };
  console.log('End');
});

Because the next function is essentially to call the next middleware through dispatch(i + 1). If the next function is not called, the next middleware cannot be executed, which represents the end of the current middleware process execution.

Call next() multiple times

app.use((ctx, next) => {
  console.log('Start');
  ctx.body = { text: 'test' };
  // Call next function multiple times
  next(); // In essence, it is dispatch(i + 1)
  next(); // In essence, it is dispatch(i + 1)
  console.log('End');
});

If next is dispatch(3), then index is 2. When next function is executed for the first time, the following logic will occur:

// index == 2
// i == 3
// No mistake.
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// After the assignment, the index is 3 
index = i

Assuming that the third middleware is the last one, the next function will execute the second one immediately after the first execution, and still execute the logic, but the index is already 3, so an error will be reported:

// index == 3
// i == 3
// Report errors
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i

Tags: Javascript github REST

Posted on Sun, 09 Feb 2020 20:13:45 -0800 by Dustin013