koa和connect框架中间件的调用实现分析

node的兴起,随之产生的各类框架也如雨后春笋般的出现。现在主流的各类node框架,主要包括koa、express、iojs等。这些框架得以应用广泛,也离不开丰富的中间件资源。一般做一个项目,我们都需要use很多的中间件进来。那么这些框架,它内部是怎样来执行这些中间件的呢?今天我们就通过对 koa 和 connect 的源码分析来一探究竟。

Connect实现分析

我们先来看个hello world的栗子,connect到底是怎么使用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var connect = require('connect');
var app = connect();
function logger(req, res, next) {
console.log('%s %s', req.method, req.url);
next();
}
function hello(req, res) {
res.setHeader('Content-Type', 'text/plain');
res.end('hello world.');
}
app.use(logger)
.use(hello);
app.listen(3000, function() {
console.log('server running on 3000.');
});

这个栗子中有两个中间件,分别是loggerhello,并通过use方法逐个加载进去。你可能已经注意到,两个中间件函数参数个数不一样,logger多了一个next参数,在输出完日志后执行了这个函数。那么我们来看下在connect内部到底是怎么实现中间件逐个执行的,这个next到底是个什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
app.use = function(route, fn) {
// default route to '/'
if ('string' != typeof route) {
fn = route;
route = '/';
}
//some code
this.stack.push({ route: route, handle: fn });
return this;
}

这段代码是use方法的实现,我们每添加一个中间件,use都会把它塞到一个事先定义好的空数组中app.stack = [];。接下来我们看下,它是怎么实现持续调用这个数组中的中间件的。

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
app.handle = function(req, res, out) {
//some code
function next(err) {
// next callback
var layer = stack[index++];
// all done
if (!layer) {
defer(done, err);
return;
}
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
}
function call(handle, route, err, req, res, next) {
try {
handle(req, res, next);
return;
} catch (e) {
err = e;
}
next(err);
}

handle内部有个next方法,首先是从stack数组中逐个取出中间件,由上面的栗子可知现在这个layer.handle就是logger函数。关键是看第12行的代码,在调用call方法的时候,把当前的next方法作为参数递归地传递了下去。在call方法内部执行了logger函数,由我们上面的logger中间件实现可知,我们在logger内部调用了next方法。这样我们就可以继续执行下一个中间件。同理,下一个取到的是hello中间件,因为在hello内部没有做next的传递,所以也就不会再执行下去了。
由上所得,一个是中间件的调用顺序和它use的顺序一致;还有就是要想能够持续触发中间件,前面的中间件必须手动执行next方法。接下来我们来看看koa又是怎么实现的呢?

Koa实现分析

我们还是用上面那个栗子,现在把它改成koa的实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var koa = require('koa');
var app = koa();
app.use(function *(next) {
var start = new Date;
yield next;
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
app.use(function *() {
this.body = 'Hello World';
});
app.listen(3000, function() {
console.log('server running on 3000.');
});

同样也是use了两个中间件,不同的是这里不再是普通的Function了,而必须是generatorFunction。和上面栗子一样,第一个中间件也有一个next参数,第二个没有。接下来我们来看下koa中use都做了些什么呢?

1
2
3
4
5
app.use = function(fn) {
//some code
this.middleware.push(fn);
return this;
}

和connect的实现一样,都是把中间件塞到一个事先定义好的空数组中this.middleware = [];。当我们调用listen方法的时候,开始了我们的处理,我们直接来看app.callback的实现:

1
2
3
4
5
6
7
app.callback = function() {
var mw = [respond].concat(this.middleware);
var gen = compose(mw);
var fn = co.wrap(gen);
//other code
}

先是把respond塞到数组队列的最前面,这里respond也是一个generatorFunction。关键是这个compose到底是做了什么呢,其实这个函数才是这里的重点,我们来看下它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function compose(middleware) {
return function *(next) {
var i = middleware.length;
var prev = next || noop();
var curr;
while (i--) {
curr = middleware[i];
prev = curr.call(this, prev);
}
yield *prev;
}
}
function* noop() {}

我们知道middleware是一个中间件的数组队列,而每一个中间件都是一个generatorFunction。首先prev初始为一个空的generator对象,在while循环中,从最后一个中间件开始调用,并把这个空的generator对象做参数传进去,同时把最后一个中间件的generator对象赋值给prev。作i--执行倒数第二个中间件,注意这时把倒数第一个的generator对象传递给了倒数第二个generatorFunction。并且prev被重新赋值为倒数第二个中间件的generator对象。以此递推,到最后这个prev的值为这个middleware数组排在第一个中间件的generator对象。也就是我们上面提到的respondgenerator对象。循环完成后,执行yield *prev,这个是代理delegating yield,执行到respond内部,在respond头部,我们看到又是一个代理yield *next;。这里的next是什么呢?前面我们说过,在作while循环的时候,后面一个中间件的generator对象会被传递到前一个genderatorFunction里来,所以这里的next就是respond后面一个中间件的generator对象,这样才能持续的执行下去。所以,我们添加的中间件内部,都要加上yield *next这一句,这样才能一直执行下去。

小结

通过 connect 和 koa 的分析得知,要想持续执行所有的中间件,都需要我们自己手动调用next方法,connect中使用next(),koa中使用yield *next。我们把需要手工调用才能持续执行后续调用的方法,叫做尾触发。详情可以查看《深入浅出Nodejs》一书的第四章。可能刚接触的时候觉得很绕,但当真正理解里面的实现原理后,才能体会到它的魅力所在,鹅妹子婴!!!