谈谈Koa 中的next

前言

最近在试着把自己写的 koa-vuessr-middleware 应用在旧项目中时,因为旧项目Koa 版本为1.2,对中间件的支持不一致,在转化之后好奇地读了一下源码,整理了一下对Koa 中next 在两个版本中的意义及相互转换的理解


正文

1.x 中的next

从Koa 的 application.js 中找到中间件部分的代码,可以看出,use 传入的中间件被放入一个middleware 缓存队列中,这个队列会经由 koa-compose 进行串联

app.use = function(fn){
  // ...
  this.middleware.push(fn);
  return this;
};
// ...
app.callback = function(){
  // ...
  var fn = this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));
  // ...
};
复制代码

而进入到koa-compose 中,可以看到compose 的实现很有意思(无论是在1.x 还是在2.x 中,2.x 可以看下面的)

function compose(middleware){
  return function *(next){
    if (!next) next = noop();

    var i = middleware.length;

    while (i--) {
      next = middleware[i].call(this, next);
    }

    return yield *next;
  }
}
// 返回一个generator 函数
function *noop(){}
复制代码

从代码中可以看出来,其实next 本身就是一个generator, 然后在递减的过程中,实现了中间件的先进后出。换句话说,就是中间件会从最后一个开始,一直往前执行,而后一个中间件得到generator对象(即next)会作为参数传给前一个中间件,而最后一个中间件的参数next 是由noop 函数生成的一个generator

但是如果在generator 函数内部去调用另一个generator函数,默认情况下是没有效果的,compose 用了一个yield * 表达式,关于yield *,可以看看 阮一峰老师的讲解;


2.x 中的next

Koa 到了2.x,代码越发精简了,基本的思想还是一样的,依然是缓存中间件并使用compose 进行串联,只是中间件参数从一个next 变成了(ctx, next),且中间件再不是generator函数而是一个 async/await 函数了

  use(fn) {
    // ...
    this.middleware.push(fn);
    return this;
  }
  // ...
  callback() {
    const fn = compose(this.middleware);
    // ..
  }
复制代码

同时, compose 的实现也变了,相较于1.x 显得复杂了一些,用了四层return,将关注点放在dispatch 函数上:

function compose (middleware) {
  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)
      }
    }
  }
}
复制代码

神来之笔在于Promise.resolve(fn(context, dispatch.bind(null, i + 1)))这一句,乍看一下有点难懂,实际上fn(context, dispatch.bind(null, i + 1)) 就相当于一个中间件,然后递归调用下一个中间件,我们从dispatch(0) 开始将它展开:

// 执行第一个中间件 p1-1
Promise.resolve(function(context, next){
  console.log('executing first mw');
  // 执行第二个中间件 p2-1
	await Promise.resolve(function(context, next){
    console.log('executing second mw');
    // 执行第三个中间件 p3-1
		await Promise(function(context, next){
      console.log('executing third mw');
      await next()
      // 回过来执行 p3-2
      console.log('executing third mw2');
    }());
    // 回过来执行 p2-2
		console.log('executing second mw2');
  })
  // 回过来执行 p1-2
	console.log('executing first mw2'); 
}());
复制代码

执行顺序可以理解为以下的样子:

// 执行第一个中间件 p1-1
first = (ctx, next) => {
  console.log('executing first mw');
  next();
  // next() 即执行了第二个中间件 p2-1
  second = (ctx, next) => {
    console.log('executing second mw');
    next();
    // next() 即执行了第三个中间件 p3-1
    third = (ctx, next) => {
      console.log('executing third mw');
      next(); // 没有下一个中间件了, 开始执行剩余代码
      // 回过来执行 p3-2
      console.log('executing third mw2');
    }
    // 回过来执行 p2-2
    console.log('executing second mw2');
  }
  // 回过来执行 p1-2
  console.log('executing first mw2'); 
} 
复制代码

从上面我们也能看出来,如果我们在中间件中没有执行 await next() 的话,就无法进入下一个中间件,导致运行停住。在2.x 中,next 不再是generator,而是以包裹在Promise.resolve 中的普通函数等待await 执行。


相互转换

Koa 的中间件在1.x 和2.x 中是不完全兼容的,需要使用koa-convert 进行兼容,它不但提供了从1.x 的generator转换到2.x 的Promise 的能力,还提供了从2.x 回退到1.x 的兼容方法,来看下核心源码:

function convert (mw) {
  // ...
  const converted = function (ctx, next) {
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  // ...
}

function * createGenerator (next) {
  return yield next()
}
复制代码

以上是从1.x 转化为2.x 的过程,先将next 转化为generator,然后使用mw.call(ctx, createGenerator(next)) 返回一个遍历器(此处传入的是* (next) => () 因此mw 为generator 函数),最后使用co.call 去执行generator 函数返回一个Promise,关于co 的解读可以参考Koa 生成器函数探寻;

接下来我们来看看回退到1.x 版本的方法

convert.back = function (mw) {
  // ...
  const converted = function * (next) {
    let ctx = this
    yield Promise.resolve(mw(ctx, function () {
      // ..
      return co.call(ctx, next)
    }))
  }
  // ...
}
复制代码

在这里,由于2.x 的上下文对象ctx 等同于1.x 中的上下文对象,即this,在返回的generator 中将this 作为上下文对象传入2.x 版本中间件的ctx 参数中,并将中间件Promise化并使用yield 返回


总结

总的来说,在 1.x 和2.x 中,next 都充当了一个串联各个中间件的角色,其设计思路和实现无不展现了作者的功底之强,十分值得回味学习

猜你喜欢

转载自juejin.im/post/5c6f9f4051882562276c4519