理解Koa洋葱模型

中间件特性

先写一段贯穿全文的koa的代码

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)

// 输出1,2,3,4,5,6

await next()使每个middleware分成,前置操作,等待其他中间件操作可以观察到中间件的特性有:

  • 上下文ctx
  • await next()控制前后置操作
  • 后置操作类似于数据解构-栈,先进后出

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');
});

从这段模拟代码我们可以知道next()返回的是promise,需要使用await去等待promise的resolve值。promise的嵌套就像是洋葱模型的形状就是一层包裹着一层,直到await到最里面一层的promise的resolve值返回。

思考:

  • 如果next()不加await执行顺序是什么呢?
    在这个例子里面如果只是next()执行顺序跟await next()是一样的,因为next的前置操作是同步的
  • 如果前置操作是异步的操作呢?

    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);
    };
    // 输出结果:1,6,2,5,3,4

    当程序执行到middleware1,执行到await p(1)等待promise值返回跳出然后到下一个事件循环时,执行next()也就是执行到middleware2,再执行到await p(2)等待promise值返回跳出middleware2,回到middleware1继续执行console.log(6),以此类推输出顺序为1.6.2.5.3.4


Promise的嵌套虽然可以实现中间件流程,但是嵌套的代码会产生可维护性和可读性的问题,也带来中间件扩展的问题。

Koa.js中间件引擎是有koa-compose模块来实现的,也就是Koa.js实现洋葱模型的核心引擎。

koa-compose 实现

  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)
      }
    }
  }
}

Koa实现的代码非常简洁,我们在使用use的时候将middleware存在一个数组里面,当拦截到请求时执行callback方法,callback中调用了compose,compose方法使用递归执行中间件,遍历完成返回promise.resolve(),实际最后执行的代码也是上面所讲的promise嵌套的形式。

扩展:深入理解babel编译后的await


通常我们的都会说await阻塞后面的操作等待promise的resolve返回值或者其他值,如果没有await这个语法糖,要怎么去实现呢?babel是怎么编译的呢
如果我们直接把代码片段一的三个中间件编译,你会发现多了regeneratorRuntime_asyncToGenerator两个函数。为了理解regeneratorRuntime我们要先理解Generator

Generator

Generator实际上是一个特殊的迭代器

let gen = null;
function* genDemo(){
  console.log(1)
  yield setTimeout(()=>{
    console.log(3);
    gen.next();// c.
  },100)
  console.log(4)
}
gen = genDemo();// a.调用generator,该函数不执行,也就是还没有输出1,返回的是指向内部状态的遍历对象。
gen.next(); // b. generator函数开始执行,输出1,遇到第一个yeild表达式停下来,调用gen.next()返回一个对象{value: 10, done:false},这里的value表示setTimeout的一个标识值,也就是调用clearTimeout的参数,是一个数字。done表示遍历还没有结束。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理