koa的中间件机制

2015-11-02 Alex Sun 更多博文 » 博客 » GitHub »

原文链接 https://syaning.github.io/2015/11/02/koa-middleware/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


本文基于koa v1.1.1

一、简单示例

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

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

二、模拟分析

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

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语句根本就没有执行到。为了解决问题,需要弄清楚以下四种情况的区别:

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内的代码不会被执行

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

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中):

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()的参数,用来处理所有请求。而与中间件相关的关键一句则是:

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)如下:

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来对上面的例子进行改写:

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 函数库的含义和用法》已经有比较详细的介绍。

关键的一个函数是:

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;
}

其中:

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

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

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]));

此时即为正确输出。