koa1.x

上一篇简要分析了koa2的源码,这一篇分析koa1的源码,koa1主要推荐使用generator function的中间件写法,由于generator function不具备自执行能力,所以还引入了tj大神的co库,我看的koa版本是1.2.5

generator

generator是es2015中定义的一个新特性,已经写入到规范中。用法如下

1
2
3
4
5
6
7
8
9
10
11
function* idMaker() {
var index = 0;
while (index < 2)
yield index++;
}
var gen = idMaker();
console.log(gen.next()); // {value:0, done:false}
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next()); // {value: undefined, done: true}

generator函数一般和yield配合使用。idMaker()调用函数的时候并不是立即执行函数,而是类似返回一个内部的指针,调用next内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。
这里每次next方法的返回值中的value都是yield表达式的值,done表示内部流程是否执行完成。
这里对比async函数可以发现两个问题:

  1. generator没有自执行能力,需要不断调用next方法
  2. next方法的调用时机要自己把握,才能完成异步操作;不像await,可以直接接受promise,通过等待promise状态改变,就很方便的执行一个个异步操作。
    从这里看,确实async函数要方便很多

koa中间件

回到koa1.x上面,给个例子说下,如何编写中间件:

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
var koa = require('koa');
var app = koa();
// x-response-time
app.use(function *(next){
// (1) 进入路由
var start = new Date;
yield next;
// (5) 再次进入 x-response-time 中间件,记录2次通过此中间件「穿越」的时间
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
// (6) 返回 this.body
});
// logger
app.use(function *(next){
// (2) 进入 logger 中间件
var start = new Date;
yield next;
// (4) 再次进入 logger 中间件,记录2次通过此中间件「穿越」的时间
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
// response
app.use(function *(){
// (3) 进入 response 中间件,没有捕获到下一个符合条件的中间件,传递到 upstream
this.body = 'Hello World';
});
app.listen(3000);
console.log("listen on 3000");

这里的执行流程和koa2是一致的。把中间件想作一个栈,请求会从顶部的第一个中间件开始处理,遇到yield next调用,就会进入下一个中间件中,直到最后没有yield next调用,再从栈底反弹,一个一个执行之前next之后的代码。

koa源码

koa源码不长,也很简洁漂亮,先看package.json,”main”:”lib/application.js”表明入口就是application.js,这里简单截取了几个重要的代码片段

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* Application prototype.
*/
var app = Application.prototype;
/**
* Expose `Application`.
*/
module.exports = Application;
/**
* Initialize a new `Application`.
*
* @api public
*/
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.proxy = false;
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
/**
* Inherit from `Emitter.prototype`.
*/
Object.setPrototypeOf(Application.prototype, Emitter.prototype);
/**/
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
/**/
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are not allowed,
// so we have to make sure that `fn` is a generator function
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
/**/
app.callback = function(){
if (this.experimental) {
console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')
}
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 handleRequest(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).then(function handleResponse() {
respond.call(ctx);
}).catch(ctx.onerror);
}
};

这里直接exports的就是Application方法。我们使用var app = new koa(),可见app就是Application的实例。通过原型继承让Application继承自Emitter。
app.listen(3000)在koa中用来创建httpServer,实际和所有的node服务器框架一样,还是基于http.createServer的封装,这里的参数this.callback()作为参数正好来处理request和response。在callback中,流程会走co.wrap(compose(this.middleware)),middleware中存放的正是使用app.use注册的一个个中间件,在use的api中,直接将多个generator方法push进入middleware数组中。

接着看compose内部代码,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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Expose compositor.
*/
module.exports = compose;
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
}
}
/**
* Noop.
*
* @api private
*/
function *noop(){}

compose执行完成返回了一个generator函数,虽然这个函数没有执行,但是看出来,这里对middleware数组从最后一个元素进行执行,但是generator function的执行只是返回pointer,并不是走内部代码逻辑,所以这里主要就是给每个中间件进行传参,越是前面定义的中间件,next中就包含了其后定义的中间件的pointer(每个middleware的next参数是不同的);架设while第一次执行返回的是lastPointer,那么循环执行结束,最终得到的是firstPointer;最终yield next,实际就是开始运行firstPointer的内部逻辑,整个中间件的串链就开始执行了。
yield
的用法是在一个generator内部执行另一个generator的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* g1(){
yield 'a';
yield 'b';
}
function* g2(){
yield 'x'
yield * g1();
yield 'y'
}
var g2i = g2();
g2i.next(); //x
g2i.next(); //a
g2i.next(); //b
g2i.next(); //y

从这里可以看出整个执行链已经组装完毕,只是等待执行了。下面看下co库的代码:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
module.exports = co['default'] = co.co = co;
/**/
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
/**/
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
/*
1.promise对象一旦建立就立即执行
2.then方法返回的是一个新的promise实例,因此可以链式调用
*/
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}

co.wrap执行是直接返回了一个createPromise函数,所以fn.call(ctx)就是直接在执行createPromise。co(gen)中的参数gen就是直接执行的compose返回的匿名generator函数。同样这个匿名generator执行只是返回了它的执行指针,gen就是这个执行指针。
co返回的是一个promise对象,promise对象是创建即执行,typeof gen === ‘function’为false,因为typeof gen===’object’并且typeof gen.next == ‘function’。在onFulfilled中,这个匿名generator真正开始执行,看下next函数中,重点就是toPromise.call(ctx, ret.value),主要就是将yield之后的值转化为promise。ret.value就是yield之后的值。转换成promise之后,再根据then调用下次的gen.next使得匿名generator中的逻辑进一步执行。
实际co就是一个generator函数的自执行器。

toPromise

toPromise函数可以详细看看,代码就在co库中

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* Convert a `yield`ed value into a promise.
*
* @param {Mixed} obj
* @return {Promise}
* @api private
*/
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;
}
/**
* Convert a thunk to a promise.
*
* @param {Function}
* @return {Promise}
* @api private
*/
function thunkToPromise(fn) {
var ctx = this;
return new Promise(function (resolve, reject) {
fn.call(ctx, function (err, res) {
if (err) return reject(err);
if (arguments.length > 2) res = slice.call(arguments, 1);
resolve(res);
});
});
}
/**
* Convert an array of "yieldables" to a promise.
* Uses `Promise.all()` internally.
*
* @param {Array} obj
* @return {Promise}
* @api private
*/
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}
/**
* Convert an object of "yieldables" to a promise.
* Uses `Promise.all()` internally.
*
* @param {Object} obj
* @return {Promise}
* @api private
*/
function objectToPromise(obj){
var results = new obj.constructor();
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
}
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}

处理里几种不同的情况

  1. obj为空,直接返回
  2. obj是promise直接返回
  3. obj是generation方法,或者是generator方法执行之后返回的pointer(这里叫generator)generator,就递归调用co方法
  4. obj是function,这种情况很常见,比如 yield readFile(); readFile是一个耗时操作,这里thunkToPromise改造readFile,返回promise,可以看出这时候readFile必须接受一个function (err, res) {}的参数
  5. obj是数组,转化为Promise.all
  6. obj是普通对象,拆分key,value。最后使用Promise.all