koa内部解密

koa

koa一直有所耳闻,没怎么用过,因为express在公司内部项目较多而且较为成熟,一直找不到使用koa的理由。最近在搭公司的npm源,看了下淘宝的cnpm,发现其是基于koa实现的。所以看了下koa框架。

使用

koa基本设计和express很类似,通过中间件来处理各个请求。但是其主要设计都是和generator有关,还是有不一样的地方,我主要不是关注使用,这部分可以参考官网:http://koajs.com/

中间件机制

koa创建一个基本的 http server如下:

1
2
3
4
var http = require('http');
var koa = require('koa');
var app = koa();
http.createServer(app.callback()).listen(3000);
  • koa()

    koa方法执行会返回一个app对象,这个构造方法位于 koa-lib-application

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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);
    }
    //构造函数返回一个新实例,然后在实例上面挂载一堆私有属性, context/request/response 对应着有lib文件夹下面的三个js,可以先简单理解为这就是三个对象,然后这三个对象上面有很多方法方便中间件使用。
  • http.createServer(app.callback())

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //app.callback就是作为原生nodejs的callback来接受事件,我们都知道它将有两个参数 req 和 res,我们看koa是怎么封装的:
    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); // ---- 封装req,res在this上
    onFinished(res, ctx.onerror); // ---绑定结束方法
    fn.call(ctx).then(function () { // 以 ctx 为 this 执行fn , 见下文
    respond.call(ctx);
    }).catch(ctx.onerror);
    }
    };
    //上面标明了入口的地方,就是原生server回调进入的第一行,我们一步一步往下。
  • fn.call(ctx)

    这个fn就是所有中间件执行的地方,很关键,我们看fn是啥:

    1
    2
    3
    4
    5
    6
    var fn = this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));
    //在非es7环境下,fn被co.wrap包裹,说明 compose(this.middleware) 将会返回一个generator.
    //从里到外分解
    //this.middleware 在application的构造函数里面我们看到他就是一个数组。数组里面的内容是什么呢,在express里面我们知道,app.use就是增加一个中间件。我们看看koa的app.use
  • app.use

    1
    2
    3
    4
    5
    6
    7
    8
    9
    app.use = function (fn) {
    if (!this.experimental) {
    // es7 async functions are allowed
    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;
    };

很简单,如果fn都是generator方法的话,直接push进去。

  • compose

    compose寥寥几行。却是整个中间件机制的重点。

    在搞清楚下面的代码之前,我们先要区分generatorFn和generator。(generator的基础我就不讲了)

    1
    2
    3
    4
    5
    6
    7
    function* generatorFn(param){
    console.log(param);
    yield 1;
    }
    var genrator = generatorFn('i am ready');
    //generatorFn就是一个generator函数,也就是我们下面通过app.use push进middleware的函数。当执行generatorFn(1)时返回的是一个generator。此时函数里面的代码并没有执行。
    //但是!此时param的值已经准备好了,param此时就等于'i am ready',当genrator.next() 一启动,马上回输出 'i am ready'

由于generatorFn执行的时候内部的代码并不会执行,所以通过这个特性可以实现把准备形参内部使用形参的这两个过程分开来。这一点将对于我们下面的理解很有帮助。我们看compose的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
compose = require('koa-compose');
//in koa-compose
function compose(middleware) {
// middleware是一个一个的generatorFn就是执行前的generator ,返回一个generatorFn,在co.wrap包裹之后,会开始执行generatorFn().next(),进入这个函数。
return function *(next) { //next = undefined
var i = middleware.length;
var prev = next || noop();
// prev等于noop
var curr;
//先从最后一个中间件开始
while (i--) {
curr = middleware[i]; //curr等于当前这个中间件
prev = curr.call(this, prev); //执行当前这个中间件,并且把上一个中间件作为形参穿进去,这就是我上面说的参数准备过程。
}
//while循环完之后的效果是,prev等于第一个中间件,每个中间件的 形参 next 准备好了,相当于把所有中间件连起来了,每个中间件的next指向下一个中间件。
yield *prev;
//依次执行中间件
}
}

上面那段代码会出现一个从末尾的中间件到开头的中间件对应的 generatorFn 执行一遍然后从开头的中间件到末尾的中间件 函数内部执行一次的奇妙过程。

路由机制

koa路由的代码比较绕,结果比较简单,路由是一个中间件,在koa-middleware里面,koa-router

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
router.param = function (param, fn) {
this.params[param] = fn;
this.routes.forEach(function (route) {
route.param(param, fn);
});
return this;
};

...

route.param = function (param, fn) {
var middleware = [];

this.fns.params[param] = function *(next) {
yield *fn.call(this, this.params[param], next);
};

this.params.forEach(function (param) {
var fn = this.fns.params[param.name];
if (fn) {
middleware.push(fn);
}
}, this);

this.middleware = compose(middleware.concat(this.fns.middleware));

return this;
};

从这个简化的模型可以看出来,最后还是通过 compose来聚合param后面的方法,依次执行,比如:

app.get(‘/‘, generatorA, generatorB);

generatorA和generatorB将和中间件一样依次执行

喝杯咖啡,交个朋友