Skip to content

koa的中间件机制

TIP

本文基于koa v1.1.1

一、简单示例

Express的中间件顺序执行不同,在koa中,中间件是所谓的“洋葱模型”。看例子:

javascript
var koa = require('koa');
var app = koa();

app.use(function* f1(next) {
	console.log('f1: pre next');
	yield next;
	console.log('f1: post next');
});

app.use(function* f2(next) {
	console.log('  f2: pre next');
	yield next;
	console.log('  f2: post next');
});

app.use(function* f3(next) {
	console.log('    f3: pre next');
	this.body = 'hello world';
	console.log('    f3: post next');
});

app.listen(4000);

输出结果为:

f1: pre next
  f2: pre next
    f3: pre next
    f3: post next
  f2: post next
f1: post next

这主要借助于generator function

二、模拟分析

下面通过一个例子来模拟上面的行为。

javascript
function* f1() {
	console.log('f1: pre next');
	yield f2;
	console.log('f1: post next');
}

function* f2() {
	console.log('  f2: pre next');
	yield f3;
	console.log('  f2: post next');
}

function* f3() {
	console.log('    f3: pre next');
	console.log('    f3: post next');
}

var g = f1();
g.next();
g.next();

输出为:

f1: pre next
f1: post next

会发现,只执行了两次next()就结束了,而且f2f3中的console.log语句根本就没有执行到。为了解决问题,需要弄清楚以下四种情况的区别:

javascript
function* outer() {
	console.log('outer: pre yield');
	// 1. yield* inner();
	// 2. yield* inner;
	// 3. yield inner();
	// 4. yield inner;
	console.log('outer: after yield');
}

function* inner() {
	console.log('inner');
}
  • yield* inner():相当于用inner的内容来替换该位置,不会消耗一次next()调用,inner内的代码会被执行
  • yield* inner:报错。因为inner是一个generator function,而yield*后面应该是一个igenerator
  • yield inner()yield的结果是一个generator,消耗一次outernext()调用,且inner内的代码不会被执行
  • yield inneryield的结果是一个generator function,消耗一次outernext()调用,且inner内的代码不会被执行

于是,将上面的模拟代码进行改动,如下:

javascript
function* f1() {
	console.log('f1: pre next');
	yield* f2();
	console.log('f1: post next');
}

function* f2() {
	console.log('  f2: pre next');
	yield* f3();
	console.log('  f2: post next');
}

function* f3() {
	console.log('    f3: pre next');
	console.log('    f3: post next');
}

var g = f1();
g.next();

输出为:

f1: pre next
  f2: pre next
    f3: pre next
    f3: post next
  f2: post next
f1: post next

这种情况下输出是正确了。然而我们会奇怪,为什么在koa中,明明是yield next,结果依然是正确的呢?看来需要对koa的源码进行分析。

三、源码分析

在koa的源码中,相关的代码为(在application.js中):

javascript
app.listen = function() {
	debug('listen');
	var server = http.createServer(this.callback());
	return server.listen.apply(server, arguments);
};

app.callback = function() {
	var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware));
	var self = this;

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

	return function(req, res) {
		res.statusCode = 404;
		var ctx = self.createContext(req, res);
		onFinished(res, ctx.onerror);
		fn.call(ctx).then(function() {
			respond.call(ctx);
		}).catch(ctx.onerror);
	}
};

app.callback()的返回值是一个函数,该函数作为http.createServer()的参数,用来处理所有请求。而与中间件相关的关键一句则是:

javascript
var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware));

不考虑this.experimental,那么重点就在co.wrap(compose(this.middleware))了。其中composekoa-compose模块,源码(v2.3.0)如下:

javascript
module.exports = compose;

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() {}

源码比较简单,其实就是compose([f1, f2, ..., fn])转化为fn(...f2(f1(noop()))),最终的返回值是一个generator function。同时也可以看出,在koa的yield next中,next是一个generator。

下面用compose来对上面的例子进行改写:

javascript
function* f1(next) {
	console.log('f1: pre next');
	yield next;
	console.log('f1: post next');
}

function* f2(next) {
	console.log('  f2: pre next');
	yield next;
	console.log('  f2: post next');
}

function* f3(next) {
	console.log('    f3: pre next');
	yield next;
	console.log('    f3: post next');
}

var compose = require('koa-compose');

var g = compose([f1, f2, f3])();
g.next();
g.next();

输出为:

f1: pre next
f1: post next

会发现,与上面的输出结果并无区别。看来重点是在co.wrap()中。

四、co

co的介绍及部分源码分析在阮一峰老师的《co 函数库的含义和用法》已经有比较详细的介绍。

关键的一个函数是:

javascript
function toPromise(obj) {
	if (!obj) return obj;
	if (isPromise(obj)) return obj;
	if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
	if ('function' == typeof obj) return thunkToPromise.call(this, obj);
	if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
	if (isObject(obj)) return objectToPromise.call(this, obj);
	return obj;
}

其中:

javascript
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);

因此,当yield的返回值是一个generator function或者generator的时候,会调用co()来执行它。因此对于上面的例子改写如下:

javascript
function* f1(next) {
	console.log('f1: pre next');
	yield next;
	console.log('f1: post next');
}

function* f2(next) {
	console.log('  f2: pre next');
	yield next;
	console.log('  f2: post next');
}

function* f3(next) {
	console.log('    f3: pre next');
	yield next;
	console.log('    f3: post next');
}

function* noop() {}

var compose = require('koa-compose');
var co = require('co');

co(compose([f1, f2, f3]));

此时即为正确输出。