koa1探秘

背景知识

掌握这些背景知识对理解koa1源码有帮助:

  • 原型链查找 闭包 高阶函数(返回函数的函数) getter/setter call/apply
  • 代理委托
  • promise 异步函数 迭代器 委托生成器(yield *)
  • co模块处理
  • node http模块

koa使用

先从koa1开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Koa = require('koa');
const app = new Koa();
// response
app.use(function *() {
console.log('1-1');
yield next;
console.log('1-2');
});
app.use(function *() {
console.log('2-1');
yield next;
console.log('2-2');
});
app.listen(3000);
// 1-1
// 2-1
// 2-2
// 1-2

上面代码加了两个中间件,执行过程是:

每一个请求到达服务器后初始化请求上下文对象,然后按照中间件添加的顺序,一个一个执行:

  • 执行中间件1,next之前的逻辑
  • next

    • 执行中间2的逻辑
  • 执行中间件1,next之后的逻辑

其他的过程都是模拟上面的流程。

koa的方法

koa有公共方法use/listen,私有方法callback等;

其中use是添加中间件,listen是创建服务、监听端口,callback初始化中间件形成闭包并返回网络请求到达时的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app.use = function(fn){
this.middleware.push(fn);
return this;
};
app.listen = function() {
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
app.callback = function(){
var fn = co.wrap(compose(this.middleware))
var self = this;
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);
}
};

koa实现原理

其实koa中间件调用过程实现原理就是根据之前写过一篇文章JS之异步1中讲述的异步生成器。

但是koa实现有所不同,koa采用的是根据promise来实现;一步一步看看上面关键callback做了什么工作;

co.wrap

上面关键代码var fn = co.wrap(compose(this.middleware))实现中间件的封装,其中co.warp其实就是返回一个函数,这个返回函数的函数体是把fn执行得到的结果作为参数传入co并执行:

1
2
3
4
5
6
co.wrap = function (fn) {
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};

compose

其中compose(this.middleware)实现的是一个迭代器的嵌套调用(这里有个背景知识,生成器函数执行是生成一个迭代器,也就是就是middleware[i].call的结果):

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

执行compose实际生成的可以看作返回了一个异步函数:

1
2
3
*function() {
return yield * itor1(itor2(itor3))
}

这个异步函数其实就是传入到co.wrap中的参数fn

再看最后一个关键函数co

co其实就是实现koa’剥洋葱’执行流程的执行体;

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
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
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);
}
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执行的时候,迭代器gen被传入,gen的值为上面compose生成的异步函数执行得到的迭代器,为啥是迭代器,因为co执行的时候,传入的是fn.call(…);

再回顾一下compose生成的异步函数:

1
2
3
\*function() {
return yield * itor1(itor2(itor3))
}

co执行过程:

  1. 返回一个promise,promise中包括的执行流程是:
  2. 如果gen是普通函数,直接执行;如果gen是不为空的值并且gen.next不为function也就是说gen不是迭代器,直接返回resolve(), 这个实际是co返回promise的resolve,也就是总的出口;
  3. gen是迭代器,执行gen.next(),也就是会执行yield * itor1(itor2(noop_itor))yield *实际上是委托生成器; 会直接执行到itor1第一个yield next处;并且此时gen.next()得到ret;并且ret.value就是next的值,也就是itor1的传入的实参itor2(itor3);
  4. 我们快要接近剥洋葱的流程了,只要itor2重复前面的流程,怎么实现呢,实际就是toPromise的工作了,且看next(ret);
  5. next函数中:如果ret.done === true, 直接退出, 这也是一个出口;否则把ret.value转换为promise对象,当ret.value是一个迭代器的时候,toPromise主要工作是重复co.call(this, ret.value); 这样就重复的执行每个中间件的yield next前面的代码,直到执行到noop并层层返回;这实际有点类似二叉树的先序遍历过程;
  6. 最后会回到第一次返回promise的函数中,并且resolve,这样就可以执行fn.call(ctx).then(function handleResponse() {respond.call(ctx);}).catch(ctx.onerror);的then处理了,否则catch处理;
  7. 自此一个网络请求后的中间件处理完毕;

总执行过程

  • 请求到来
  • 执行fn.call(ctx),实际上就是执行co.wrap(compose(this.middleware)).call(ctx), 而co.wrap(...).call(ctx)实际就是上面的co函数的执行过程;
  • 中间件处理完毕, 然后执行respond.call(ctx);

koa的上下文

koa服务每当一个新的网络请求来临的时候会生成一个新的上下文,也就是代码中的多次出现的ctx;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var ctx = self.createContext(req, res);
app.createContext = function(req, res){
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var 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.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
context.accept = request.accept = accepts(req);
context.state = {};
return context;
};

这个context是以koa的实例服务app.context为原型,并且对ctx(即context)的属性request、response也设计了原型,这有什么用处呢?

实际上我们获取ctx.header、ctx.method或者设置ctx.method等的值,就是通过这里来的;举一个例子,获取ctx.header

  • 1 ctx中没有,从原型app.context中寻找
  • 2 app.context从静态类context中寻找

    1
    2
    3
    4
    5
    6
    7
    function Application() {
    if (!(this instanceof Application)) return new Application;
    ...
    **this.context = Object.create(context);**
    this.request = Object.create(request);
    this.response = Object.create(response);
    }
  • 3 context中的header代理了request静态类的属性header;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    delegate(proto, 'request')
    .method('acceptsLanguages')
    ... // 省略
    .getter('header')
    ...
    .getter('ip');
    Delegator.prototype.getter = function(name){
    var proto = this.proto;
    var target = this.target;
    this.getters.push(name);
    proto.__defineGetter__(name, function(){
    return this[target][name];
    });
    return this;
    };
  • 4 也就是ctx[‘request’][‘header’]

  • 5 寻找ctx[‘request’][‘header’]也就是从ctx[‘request’]的原型app.request寻找,接着到静态类request中寻找

    1
    2
    3
    get header() {
    return this.req.headers;
    },
  • 6 也就是return ctx.request.req.headers, 其中ctx.request.req实际就是网络请求传入的req对象

  • 7 获取header成功

koa利用原型链查找、委托代理等手段,设置了很多方面快捷的属性获取、属性设置和方法调用;

总结

本文讲述了下面几方面内容:

  • koa简单使用
  • koa中间件执行过程
  • koa中间件执行原理
  • koa上下文ctx
  • koa快捷过程

不过koa也有不足之处,如果上层中间件设置了多次yield next,虽然后面的迭代器已经执行到done:true,但每次重新到下层中间件也是一种不必要的开销;koa1的代码也比较繁杂,理解起来比较困难;不过这些koa2都有所改善,koa2见。