Koa源码解读

// 第一步 - 初始化app对象
var koa = require('koa');
var app = koa();
// 第二步 - 监听端口
app.listen(1995);

初始化

执行koa()的时候初始化了一些很有用的东西,包括初始化一个空的中间件集合,基于Request,Response,Context为原型,生成实例等操作。Request和Response的属性和方法委托到Context中也是在这一步进行的。在这一步并没有启动Server。

module.exports = class Application extends Emitter {
    /**
    * Initialize a new 'Application'.
    *
    * @api public
    */
    constructor() {
        super();
        this.proxy = false;
        this.middleware = [];
        this.subdomainOffset = 2;
        this.env = process.env.NODE_ENV || 'development';
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
    }
    ...

几项配置的含义:
env 环境,默认为 NODE_ENV 或者 development
proxy 如果为 true,则解析 "Host" 的 header 域,并支持 X-Forwarded-Host
subdomainOffset 默认为2,表示 .subdomains 所忽略的字符偏移量。

启动Server

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

在执行app.listen(1995)的时候,启动了一个server,并且监听端口。
http.createServer接收一个函数作为参数,每次服务器接收到请求都会执行这个函数,并传入两个参数(request和response,简称req和res),那么现在重点在this.callback这个方法上。

callback

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

上述代码完成了两件事情:初始化中间件,接收处理请求。

初始化中间件

其中,compose的全名叫koa-compose,他的作用是把一个个不相干的中间件串联在一起。

// 有3个中间件
this.middlewares = [function *m1() {}, function *m2() {}, function *m3() {}];
// 通过compose转换
var middleware = compose(this.middlewares);
// 转换后得到的middleware是这个样子的
function *() {
yield *m1(m2(m3(noop())))
}

上述是V1的代码,跟V2意思差不多。generator函数的特性是,第一次执行并不会执行函数里的代码,而是生成一个generator对象,这个对象有next,throw等方法。
这就造成了一个现象,每个中间件都会有一个参数,这个参数就是下一个中间件执行后,生成出来的generator对象,没错,这就是大名鼎鼎的 next。
那compose是如何实现这样的功能的呢?我们看一下代码:

module.exports = compose
function compose(middleware) {
    if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
    }
    return function (context, next) {
        // last called middleware #
        let index = -1
        return dispatch(0)
        function dispatch(i) {
            if (i <= index) return Promise.reject(new Error('next() called multiple times'))
            index = i
            let fn = middleware[i]
            if (i === middleware.length) fn = next
            if (!fn) return Promise.resolve()
            try {
                return Promise.resolve(fn(context, function next() {
                    return dispatch(i + 1)
                }))
            } catch (err) {
                return Promise.reject(err)
            }
        }
    }

这里的逻辑就是:先把中间件从后往前依次执行,并把每一个中间件执行后得到的值赋值给变量next,当下一次执行中间件的时候(也就是执行前一个中间件的时候),把next传给第二个参数。这样就保证前一个中间件的参数是下一个中间件生成的值,第一次执行的时候next为underfined。



compose(this.middleware)() = this.middleware[0](context, this.middleware[1](context, this.middleware[2](context, null)))
有一个问题,什么时候会出现i <= index的情况?答案是当一个中间件中两次调用next时。比如,当第二个中间件里两次调用next,那执行结果就变成了这样。

可以看出,当第二次执行next时,index===i,就会抛出异常。

注意:一个中间件里是不能多次调用next的。

接收请求

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

创建上下文

var ctx = self.createContext(req, res);

对应的源码是:

/**
* Initialize a new context.
*
* @api private
*/
createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const 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.originalUrl = request.originalUrl = req.url;
    context.cookies = new Cookies(req, res, {
        keys: this.keys,
        secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
}

koa中的this其实就是app.createContext方法返回的完整版context
又由于这段代码的执行时间是接受请求的时候,所以表明:
每一次接受到请求,都会为该请求生成一个新的上下文
可以看看,经过这一步处理后他们之间的关系是怎样的,可以用这个图来表示:

从上图中,可以看到分别有五个箭头指向ctx,表示ctx上包含5个属性,分别是request,response,req,res,app。request和response也分别有5个箭头指向它们,所以也是同样的逻辑。


介绍一下这几个概念:
ctx,就是上下文,context 在每个 request 请求中被创建,在中间件中作为接收器(receiver)来引用,或者通过 this 标识符来引用:

app.use(function *(){
    this; // is the Context
    this.request; // is a koa Request
    this.response; // is a koa Response
});

node里有request和response两个对象,分别有处理请求和响应的API。在koa里,将这两个对象封装在了ctx里,可以通过ctx.req(=noderequest)和ctx.res(=node request)来使用。
Koa Request 对象(=ctx.request)是对 node 的 request 进一步抽象和封装,提供了日常 HTTP 服务器开发中一些有用的功能。
Koa Response 对象(=ctx.response)是对 node 的 response 进一步抽象和封装,提供了日常 HTTP 服务器开发中一些有用的功能。
许多 context 的访问器和方法为了便于访问和调用,简单的委托给他们的 ctx.request 和 ctx.response 所对应的等价方法,比如说 ctx.type 和 ctx.length 代理了 response 对象中对应的方法,ctx.path 和 ctx.method 代理了 request 对象中对应的方法。
app是应用实例引用。
具体API看http://koa.bootcss.com/


错误监视

onFinished(res, ctx.onerror);

这行代码的作用是监听response,如果response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等。

执行中间件

fn.call(ctx).then(function () {
    respond.call(ctx);
}).catch(ctx.onerror);

fn是compose(初始化中间件)执行后的结果,fn.call(ctx)执行中间件逻辑,执行成功则接着执行response.call(ctx),否则进行错误处理。
请求的时候会经过一次中间件,响应的时候又会经过一次中间件。


var koa = require('koa');
var app = koa();
app.use(function* f1(next) {
    console.log('f1: pre next');
    yield next;
    console.log('f1: post next');
    yield next;
    console.log('f1: fuck');
});
app.use(function* f2(next) {
    console.log(' f2: pre next');
    yield next;
    console.log(' f2: post next');
    yield next;
    console.log(' f2: fuck');
});
app.use(function* f3(next) {
    console.log(' f3: pre next');
    yield next;
    console.log(' f3: post next');
    yield next;
    console.log(' f3: fuck');
});
app.use(function* (next) {
    console.log('hello world')
    this.body = 'hello world';
});
app.listen(3000);

打印如下:

f1: pre next
f2: pre next
f3: pre next
hello world
f3: post next
f3: fuck
f2: post next
f2: fuck
f1: post next
f1: fuck

用一张图表示如下:



由于每次接收请求,都会执行callback,所以每次接收请求以下操作都会被执行:

  • 初始化中间件
  • 生成新的上下文
  • 执行中间件逻辑
  • 响应请求或错误处理
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容