浅析Koa2源码中洋葱模型的实现

前言

​ 最近刷知乎中看到关于koa的问题koa2框架中的中间件同步还是异步的问题? 由此去探索koa2中间件的源码实现

先来看一个koa的demo看看效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Koa = require('koa')

const app = new Koa();

app.use(async (ctx, next)=>{
console.log(1)
await next();
console.log('1-1')
});

app.use(async (ctx, next) => {
console.log(2)
await next();
console.log("2-1")
})

app.use(async (ctx, next) => {
console.log(3)
})

app.listen(3000,function(){
console.log("3000")
})

我们可以看出输出的结果为:

1
2
//端口号3000
1 2 3 2-1 1-1

我们从结果上看是不是觉得当遇到await next()是,async异步函数就会停止当前程序,进入到下一个中间件,然后当最后一个中间件执行完时,再回到上一个中间件执行程序,一直回滚上去。我想大家会十分好奇为什么会这样,对其中中间件也有很多疑问吧。那注意咯!我们要开车咯

koa的源码解析

翻看源码可以看到:

在源码application.js中koa中定义了一个listen()的监听服务器端的回调函数.

1
2
3
4
5
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

从源码中可以看到app.listen使用了this.callback()来生成node的httpServer的回调函数。

那让我们看看this.callback()函数到底做了什么事?

1
2
3
4
5
6
7
8
9
10
11
12
callback() {
const fn = compose(this.middleware);

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;
}

函数里面有一个变量this.middleware,可想而知这个可能是中间件集合;同时函数里面还对请求request和响应response做了封装。这个调用了compose处理了this.middleware。

1
this.middleware = [];//中间件集合

说到中间件数组,那么我们看看是怎么添加中间件的:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

是定义了一个use函数去添加中间件,this.middleware.push(fn)

添加完中间件之后,那中间件之间的单向跳转又是怎么回事呢???compose又是什么???跟踪源码在koa-compose模块可以看到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)
}
}
}
}

在compose中用到了递归,最为关键的是dispatch()函数,它遍历了所有中间件middleware,最后不管中间件返回的对象是不是Promise对象,它都将通过Promise.resolve()包装成Promise。然后将context和next函数作为参数传给中间件middleware中的方法。

1
2
3
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))

通过这一段代码和递归函数实现了koa中间件的洋葱模型。这段代码它将context一直传递给下一个中间件,将下一个中间件作为next()函数的返回值。

看完了koa中间件的核心,不要马上关掉这个页面,下面会让你更加惊喜。s

再回到开始,再看看createContext函数和handleRequest函数,前一个是将请求request和响应response做了封装,后一个对一些数据做处理。

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
/**
* Handle request in callback.
*
* @api private
*/

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);
}

/**
* Initialize a new context.
*
* @api private
*/

createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
request.ip = request.ips[0] || req.socket.remoteAddress || '';
context.accept = request.accept = accepts(req);
context.state = {};
return context;
}

最后你可以再思虑一下,就会明白其中的原理。不说了,我先下车咯。你们继续!

参考链接

koa源码


本文由 Abert 创作,采用 知识共享署名 4.0 国际许可协议。

本站文章除注明转载/出处外,均为本站原创或翻译,转载请务必署名。