koa2中间件原理

今天说说koa2中next的原理

举个栗子🌰

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
41
42
43

const app = {
middleware: [],
callback(ctx) { //最终请求结束调用
console.log(ctx)
},
use(fn) {
// 逻辑
this.middleware.push(fn)
},
start(ctx) {
// 逻辑
const reducer = (next, fn , i) => () => {
console.log(fn, next)
fn(ctx, next)
}
// 从右侧遍历,最后拿到最栈顶的next,并从栈顶到栈尾形成next链
const q = this.middleware.reduceRight(reducer, this.callback.bind(this, ctx)) //没处理异步
console.log(q)
// 开始执行栈顶
q()
}
}
app.use((ctx, next) => {
ctx.name = 'huangwei'
console.log('>> use name')
next()
console.log('<< use name')
})
app.use((ctx, next) => {
ctx.age = 20
console.log('>> use age')
next()
console.log('<< use age')
})

app.use((ctx, next) => {
console.log(`>> ${ctx.name} is ${ctx.age} years old`)
next()
console.log(`<< ${ctx.name} is ${ctx.age} years old`)
})

app.start({})

输出结果:

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
41
42
43
44
// q
() => {
console.log(fn, next)
fn(ctx, next)
}
(ctx, next) => {
ctx.name = 'huangwei'
console.log('>> use name')
next()
console.log('<< use name')
}
() => {
console.log(fn, next)
fn(ctx, next)
}
>> use name

(ctx, next) => {
ctx.age = 20
console.log('>> use age')
next()
console.log('<< use age')
}
() => {
console.log(fn, next)
fn(ctx, next)
}
>> use age

(ctx, next) => {
console.log(`>> ${ctx.name} is ${ctx.age} years old`)
next()
console.log(`<< ${ctx.name} is ${ctx.age} years old`)
}
ƒ callback(ctx) { //最终请求结束调用
console.log(ctx)
}
>> huangwei is 20 years old

{name: "huangwei", age: 20}

<< huangwei is 20 years old
<< use age
<< use name

分析栗子🌰

先看一张图

中间件是怎么串起来的

首先我们看到上面有黄色的模块,这个我们定义为我们刚刚使用的中间件默认执行函数callback(D),三个中间件一次为A、B、C,那么我们下面的两个条状柱体是reduceRight中reducer回调函数的前两个参数
那我们看到的应该是这样的。

第一次回调分别是D、C,因为我们后面传入了默认值D,第二次是返回第一次reducer返回的函数,第三次是返回第二次reducer返回的函数,以此类推,就生成了这个图。然后就形成了联调,那么最后拿到的q就是最后的A->B的函数,这样的话就形成链条(链表),然后我们顺着缕缕接下来的过程。A执行的时候,A里面包含next,而这个next正好是B,B执行的时候,B里面next正好是C,C执行的时候,C里面的next正好是D,好,这个时候D执行完,往上回溯,执行C的next后面的逻辑,这个时候C执行结束,到B的next后面逻辑,最后到A的next后面逻辑,最后就形成了这个神奇的next串

注:这里是没有考虑异步

直接koa2

上面是模拟koa2,下面直接使用koa2

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

const one = async (ctx, next) => {
console.log('>> one');
// return next();
await next();
console.log('<< one');
}

const two = async (ctx, next) => {
console.log('>> two');
// return next();
await next();
console.log('<< two');
}

const three = async (ctx, next) => {
console.log('>> three');
// return next();
await next();
console.log('<< three');
}

app.use(one);
app.use(two);
app.use(three);

app.listen(3000);

输出结果:

1
2
3
4
5
6
>> one
>> two
>> three
<< three
<< two
<< one

当然这和我们的next放的位置有关系 放最后就不会出现回溯,最前面的话,表面看就只有回溯

如果打开上面return
则输出结果为:

1
2
3
>> one
>> two
>> three

这样的话可以阻止会面的回溯,通常也会这样写,当然也有特殊case中需要用到这个特性,比如
记录请求的总耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.use( (ctx, next) => {

const startTime = Date.now()
const { request, response } = ctx

ctx.request.uuid = ctx.headers.UNIQID || uuid()
ctx.request.sequence = 0

return next().then(() => {
const endTime = Date.now()
process.emit('log.access', {
request, response, startTime, endTime, ctx,
uuid: ctx.request.uuid,
sequence: ctx.request.sequence,
timeCost: endTime - startTime
})
})
})

关于上面的运行机制及原理,我们分析一波
我们关注下application.js
application传送门

1
2
3
4
5
6
7
8
9
10
11
12
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}

重点关注this.middleware.push(fn),向我们的middleware栈push中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
callback() {
const fn = compose(this.middleware);

if (!this.listenerCount('error')) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};

return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

重点关注
const fn = compose(this.middleware)
和handleRequest中的
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
那重点就在compose,compose函数返回的是一个promise

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
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,这样结合上面的看会发现形成了一个递归链,这样也就解释了为什么会出现回溯

当next为空,直接返回,所以也就解释了为什么没有next不会向下走。
假设我们执行到第一个fn了,这个时候fn是

1
2
3
4
5
6
fn = async (ctx, next) => {
console.log('>> one');
// return next();
await next();
console.log('<< one');
}

next对应 function next(){return dispatch(i+1)},所以就进入到dispatch(i+1)下一个中间件了,以此类推。
核心就是dispatch,最后结束就是next不存在,然后开始依次回溯到里层,所以执行顺序是越先注册越后执行。