最近在试着把本身写的 koa-vuessr-middleware 应用在旧项目中时,由于旧项目Koa 版本为1.2,对中间件的支持不一致,在转化以后好奇地读了一下源码,整理了一下对Koa 中next 在两个版本中的意义及相互转换的理解javascript
从Koa 的 application.js 中找到中间件部分的代码,能够看出,use 传入的中间件被放入一个middleware 缓存队列中,这个队列会经由 koa-compose
进行串联html
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 能够看下面的)vue
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
函数生成的一个generatorjava
可是若是在generator 函数内部去调用另外一个generator函数,默认状况下是没有效果的,compose 用了一个yield *
表达式,关于yield *
,能够看看 阮一峰老师的讲解;git
Koa 到了2.x,代码愈加精简了,基本的思想仍是同样的,依然是缓存中间件并使用compose 进行串联,只是中间件参数从一个next
变成了(ctx, next)
,且中间件再不是generator函数而是一个 async/await 函数了es6
use(fn) {
// ...
this.middleware.push(fn);
return this;
}
// ...
callback() {
const fn = compose(this.middleware);
// ..
}
复制代码
同时, compose 的实现也变了,相较于1.x 显得复杂了一些,用了四层return,将关注点放在dispatch
函数上:github
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');
}());
复制代码
执行顺序能够理解为如下的样子:app
// 执行第一个中间件 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
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 都充当了一个串联各个中间件的角色,其设计思路和实现无不展示了做者的功底之强,十分值得回味学习