koa的中间件机制原理及async await版本实现

个人还是很喜欢这种机制,也想搞一个这般的 Workflow 辅助类出来,不过不想用 Generator,所以折腾了一小下……

不得不说的 yield *

yield*最大的作用,就是将Generator嵌在另一个Generator的内部执行。

1
2
3
4
5
6
7
8
function *a() {
console.log(1);
b();
console.log(3);
}
function *b() {
console.log(2);
}

执行结果是13,是的,2不会被输出,但是如果换成:

1
yield *b();

就等同于:

1
2
3
4
5
function *a() {
console.log(1);
console.log(2);
console.log(3);
}

输出就是123就好了。

而这也是koa2的中间件核心实现机制,笑话一下自己最开始搞出来的遍历行为,傻在next上了,囧~~

async/await模式的实现

如果还不明白,那么就看这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
function *a() {
console.log(1);
yield* b();
console.log(5);
}
function *b() {
console.log(2);
yield* c();
console.log(4);
}
function *c() {
console.log(3);
}

循环内嵌之后,你只需要遍历a的Iterator就行了,会展开嵌入后面所有的Generator!!

所以核心问题就变成了,如何定义出所需要的方法a而已。

koa的middlewares是一个数组,为了能够得到要访问的方法,其实就是middlewares[0]的封装。我们知道每个async function其实就是一个Generator function,那么我只需要把参数next指向下一个中间件函数,并且确保调用时使用yield*去调用,并且参数绑定ctx和下一个next。

所以我需要的可能是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let nextc = function* () {
console.log(3);
}
let nextb = function* () {
console.log(2);
yield* nextc();
console.log(4);
}
let nexta = function* () {
console.log(1);
yield * nextb();
console.log(5);
}
nexta().next(); // 执行,输出 1 2 3 4 5

再换一下,用async/await:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 注意这里我开始引入 next 了
async function a(next) {
console.log(1);
await next();
console.log(5);
}
async function b(next) {
console.log(2);
await next();
console.log(4);
}
async function c(next) {
console.log(3);
}
// 然后我们来手动定义next
let nextc = async function () {
c.call(this);
};
let nextb = async function () {
b.call(this, nextc);
}
let nexta = async function () {
a.call(this, nextb);
}
// 执行
nexta();
// 输出了 1 2 3 4 5

发现规律了么……

nexta调用的是函数a,将nextb作为next参数传入,而nextb调用b,将nextc作为参数传入,同理继续。最后一个指向空就行了,啥也不干。

那么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function compose(middlewares) {
let i = middlewares.length;
let next = async function(){}; // noop
// 循环算出来next
for (let i = middlewares.length - 1; i >=0; i--) {
next = getCallback(middlewares[i], next);
}
return next;
}
function getCallback(method, next) {
return async function() {
method(next);
};
}
let callback = compose([a, b, c]);
callback(); // 输出1 2 3 4 5

貌似大功告成?还得继续……

中间件是需要传入数据的,简单理解为:一个数据要从第一个中间件进入,最终从第一个中间件的after出来,核心目的是改变这个数据,因此在当前的模式下,最好不要传值,我们来看:

1
2
3
4
5
app.use(async (ctx, next) => {
// do sth with ctx
await next();
// do sth with ctx after
});

根据上面的调用模式,如果一直是值引用,那么就要 return 处理后的值,与代码中的await next()不符了,而且使用还会变得麻烦,所以koa选择使用context的概念,传入一个Object的引用,然后一直去修改它。

那么我们接下来要做的事情,其实就是支持 ctx 参数了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 定义中间件,这看起来就跟 koa 一样了是吧
async function a(ctx, next) {
ctx.output.push(1);
await next();
ctx.output.push(5);
}
async function b(ctx, next) {
ctx.output.push(2);
await next();
ctx.output.push(4);
}
async function c(ctx, next) {
ctx.output.push(3);
}
// 定义compose,因为参数的位置变化了,所以下面的执行也要修改一下了
// 最简单的方式,加一个参数
function compose(middlewares, ctx) {
let i = middlewares.length;
let next = async function(){}; // noop
// 循环算出来next
for (let i = middlewares.length - 1; i >=0; i--) {
next = getCallback(middlewares[i], ctx, next);
}
return next;
}
function getCallback(method, ctx, next) {
return async function() {
method(ctx, next);
};
}
let ctx = {output: []};
let callback = compose([a, b, c], ctx);
await callback();
console.log(ctx.output); // 输出1 2 3 4 5

能不能更简便些呢?

好吧,反正我还是没看明白 koa 的 compose 结果,怎么被使用的,直接传一个 ctx 作为参数就达到效果了。

我其实怀疑是 koa-convert 搞的鬼,默认都是 Generator,都被转换成 async 了,然后 call 了ctx 和 next。

应该是这样吧……