洋葱圈的实现

我们这样使用中间件:

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
const Koa = require('koa');
const app = new Koa();

// logger

app.use(async (ctx, next) => {
await next(); // 1
const rt = ctx.response.get('X-Response-Time'); // 2
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time

app.use(async (ctx, next) => {
const start = Date.now(); // 3
await next(); // 4
const ms = Date.now() - start; // 5
ctx.set('X-Response-Time', `${ms}ms`);
});

// response

app.use(async ctx => {
ctx.body = 'Hello World'; // 6
});

app.listen(3000);

中间件都是async函数,koa能够保证这些中间件是按照你加入中间件的顺序执行,这到底是怎么做到的?app.use的方法将所有的函数放入了自身维护的middleware数组。app.listen创建了一个httpServer并将onRequest回调指定为app.callback,app.callback主要做了2件事:添加onerror,处理用户请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
callback() {
// 组合中间件
const fn = compose(this.middleware);
// 处理onerror
if (!this.listeners('error').length) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
// 请求处理器
return handleRequest;
}

可以看出组合中间件的方法中使用了compose,这个函数的实现如下(在koa-compose模块中):

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
function compose(middleware) {
// 检测
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}

/**
* @param {Object} context
* @return {Promise}
* @api public
*/

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, function next() {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}

以上的高阶函数内部递归调用了dispatch函数,一直迭代所有的中间件。本文开篇的那个例子中1处的await next将代码的执行权交给了2处记录下开始时间,接着执行4处的await,将代码的执行权交给了6接下来4处的await执行完了执行5,最后1处的await执行完了,至此中间完成了。

原理如下:

首先,我们在写中间时会有await next()的用法(注意,await会等到后面的Promise resolvereject后才厚向下继续执行),那么执行await next()就会转而执行dispatch(i + 1) ,直到最后一个中间件;当执行到最后一个再执行dispatch时,会触发if(!fn) return Promise.resolve(), 最后一个中间件开始执行await next()后面的逻辑,完成后,执行倒数第二个,依次执行到第一个中间件。

注意,当中间件中有两处await next()时,会触发if(i <= index) return Promise.reject(new Error('next() called multiple times')), 抛出错误。

这个过程有点像深度优先遍历,递归到树的最深处,然后逐层返回,最后的结果是初始调用第一层的结果(对应在koa中是我们use的第一个中间件)。其实和下面的代码是一个意思:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function fn1(next) {
console.log('fn1 start');
next();
console.log('fn1 end');
return 1;
}

function fn2(next) {
console.log('fn2 start');
next();
console.log('fn2 end');
return 2;
}

function fn3() {
console.log('fn3');
return 3;
}

const ret = fn1(() => fn2(() => fn3()));
console.log(ret);

输出:

1
2
3
4
5
6
fn1 start
fn2 start
fn3
fn2 end
fn1 end
1

整个函数的返回值是最外层函数的值。改写成递归就是如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fns = [fn1, fn2, fn3];

function compose(fns) {

function dfs(i) {
let fn = fns[i];
if (!fn) return;
return fn(function next() {

return dfs(i + 1);
});
// 可以进一步简化
// return fn(dfs.bind(null,i+1));
}

return dfs(0);
}

compose(fns);