Understanding Koa onion model

Keywords: Javascript

Middleware features

Write a piece of koa code that runs through the whole article.

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

const middleware1 = async (ctx, next) => { 
  console.log(1); 
  await next();  
  console.log(6);   
}

const middleware2 = async (ctx, next) => { 
  console.log(2); 
  await next();  
  console.log(5);   
}

const middleware3 = async (ctx, next) => { 
  console.log(3); 
  await next();  
  console.log(4);   
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.use(async(ctx, next) => {
  ctx.body = 'hello world'
})

app.listen(3001)

// Output 1, 2, 3, 4, 5, 6

await next() divides each middleware into pre-operation and wait for other middleware operations to observe the characteristics of the middleware:

  • Context ctx
  • await next() controls pre-and post-operation
  • Post-operation is similar to data deconstruction-stack, FIFO

Simulation Implementation of promise

Promise.resolve(middleware1(context, async() => {
  return Promise.resolve(middleware2(context, async() => {
    return Promise.resolve(middleware3(context, async() => {
      return Promise.resolve();
    }));
  }));
}))
.then(() => {
    console.log('end');
});

From this simulation code, we can see that next() returns promise, and we need to use await to wait for the resolution value of promise. The nesting of promises is like the shape of an onion model, which is wrapped in a layer until await returns to the resolve value of the promise in the innermost layer.

Reflection:

  • What is the execution order of next() without await?
    In this example, if only next() is executed in the same order as await next(), because the pre-operation of next is synchronous.
  • What if the pre-operation is an asynchronous operation?

    const p = function(args) {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log(args);
          resolve();
        }, 100);
      });
    };
    
    const middleware1 = async (ctx, next) => {
      await p(1);
      // await next();
      next();
      console.log(6);
    };
    
    const middleware2 = async (ctx, next) => {
      await p(2);
      // await next();
      next();
      console.log(5);
    };
    
    const middleware3 = async (ctx, next) => {
      await p(3);
      // await next();
      next();
      console.log(4);
    };
    // Output: 1, 6, 2, 5, 3, 4

    When the program executes to middleware 1, waits until await p(1) returns the promise value to jump out and then goes to the next event loop, executes next() that is, to middleware 2, then to await p(2), waits for the promise value to return to jump out of middleware 2, returns to middleware 1 and continues to execute console.log(6), and so on output order is 1.6.2. .5.3.4

Although the nesting of Promise can realize middleware process, the nested code will cause problems of maintainability and readability, and also bring problems of middleware extension.

Koa.js middleware engine is implemented by koa-compose module, which is the core engine of Koa.js onion model.

koa-compose implementation

  this.middleware = [];
  use(fn) {
    this.middleware.push(fn);
    ......
 }
 callback() {
    const fn = compose(this.middleware);
    ......
 }
 
function compose (middleware) {
  return function (context, next) {
    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)
      }
    }
  }
}

The code implemented by Koa is very simple. When using use, we store Middleware in an array. When intercepting a request, we execute the callback method. Compoose is called in the callback. The compose method uses the recursive execution middleware to complete the traversal return to promise.resolve(), and the last code actually executed is the same as above. The promise nested form.

Extension: Deep understanding of babel compiled await

Usually we will say that the operation after await blocking waits for promise's resolve return value or other value. If there is no await grammatical sugar, how to implement it? How does babel compile?
If we go directly to the three middleware fragments of code Compile You will find two more functions, regenerator Runtime and _asyncToGenerator. To understand regenerator Runtime, we need to understand Generator first.

Generator

Generator is actually a special iterator

let gen = null;
function* genDemo(){
  console.log(1)
  yield setTimeout(()=>{
    console.log(3);
    gen.next();// c.
  },100)
  console.log(4)
}
gen = genDemo();// A. Call generator, the function does not execute, that is, it has not yet output 1, returning a traversal object to the internal state.
gen.next(); // b. generator function starts to execute, output 1, encounters the first year expression, stops, calls gen.next() and returns an object {value: 10, done:false}, where value represents an identifying value of setTimeout, that is, the parameter calling clearTimeout is a number. Done means that the traversal is not over yet.

Posted by birwin on Sun, 28 Jul 2019 02:11:24 -0700