Koa源码解读

阅读koa的源码,发现它真是短小精悍,其中的精髓应该要属基于async的中间件机制和错误处理了。所以我就从这两方面去解读源码。

Middleware机制

首先在Application类的构造函数内部初始化对象的middleware属性为空数组(空栈):

1
2
3
4
5
6
7
8
9
10
11
constructor() {
super();

this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}

当调用use()方法时,压入新的中间件函数到middleware栈顶:

1
2
3
4
5
use(fn) {
//...
this.middleware.push(fn);
return this;
}

执行中间件:

一切的一切从listen()开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

// callback():
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;
}

将中间件栈内的函数转化为原生httpServer能够执行的回调,其逻辑在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
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, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}

从第一个middleware开始,通过Promise.resolve()async函数转化为Promise对象,同时传入的next函数返回下一个middleware转化后的Promise对象,这样递归进行。

最终在handleRequest()函数里,创建context,调用this.handleRequest():

1
2
3
4
5
6
7
8
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);
}

context对象传入经过compose()返回的函数,然后这个函数再返回Promise对象,handleRequest()函数再加入了最后的response逻辑和错误处理。

最后自己实现一个简单的koa并测试,只包含middleware处理逻辑:

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
45
46
47
48
49
 class Koa {
constructor(){
this.middleware = [];
}
listen(){
let cb = this.callback();
cb();
}
use(fn){
this.middleware.push(fn);
}
callback(){
let index = -1;
let middleware = this.middleware;

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

let ko = new Koa();
ko.use(async next=>{
console.log(1);
await next();
console.log(5);
});
ko.use(async next=>{
console.log(2);
await next();
console.log(4);
});
ko.use(async next=>{
console.log(3);
});

ko.listen();

最终输出顺序为1 2 3 4 5,正确运行。

错误处理

try...catch

1
2
3
4
5
6
7
8
9
10
11
12
app.use(async function(ctx,next){
try {
await next();
} catch(e){
console.error('catched error:',e);
}
});


app.use(async function(ctx,next){
throw new Error('some error')
});

这是怎么做到的呢?其实根据上面我们分析的compose模块代码,其实上面的逻辑可以转化为下面的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getNext(){
try{
return Promise.resolve((async ()=>{
throw new Error("some error");
})());
} catch(e){
console.error('error from Promise.resolve()',e);
}
}
async function handleRequest() {
try {
let num1 = await Promise.resolve(5);
let num2 = await getNext();
} catch(e){
console.log('错误来了');
console.error('catched error:',e);
}
}

Promise.resolve(handleRequest());

最终结果一样。需要注意的是,如果Promise对象本身就进行了catchPromise.resolve().catch(),那么错误就是由Promise自己处理,在调用它的中间件里就捕获不到该错误了(除非在catch内部再次将错误抛出)。

关于generatorasync混合使用:

1
2
3
4
5
6
7
8
9
10
11
function *ge(){
console.log('this is generator');
throw new Error("generator error");
}
app.use(async function(ctx,next){
try {
await ge();
} catch(e){
console.error('catched error:',e);
}
});

发现这样是没法捕获到generator内部的错误的,而在koa1中,中间件函数都为generatorco模块实现了类似于原生async函数的try...catch错误处理机制:

1
2
3
4
5
6
7
8
9
10
11
app.use(function *(next){
try {
yield next;
} catch(e){
console.error('catched error:',e);
}
});

app.use(function *(next){
throw new Error('some error');
});

所以最好不要asyncgenerator混用。然后koa2也不建议使用generator中间件函数,并在koa3中取消支持。

全局错误 onerror

在前面分析的this.handleRequest()方法内:

1
2
3
4
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);

可以看到,全局处理调用的是ctx.onerror()方法,该方法实现在lib/context.js中,其触发了error事件: this.app.emit('error', err, this);

所以,就需要有error事件的listener,在this.callback()方法中,定义了默认的全局错误处理函数:

1
if (!this.listeners('error').length) this.on('error', this.onerror);

当然最好是自定义:

1
2
3
4
5
6
7
app.use(async function(ctx,next){
throw Error("some error");
});

app.on('error',e=>{
console.error('onerror:',e);
});

默认错误代码为500

罗峡的博客 wechat
欢迎扫描上面的微信公众号二维码,关注我的个人公众号:全栈前端
坚持原创技术分享,您的支持将鼓励我继续创作!