Koa源碼解讀

// 第一步 - 初始化app對象
var koa = require('koa');
var app = koa();
// 第二步 - 監(jiān)聽端口
app.listen(1995);

初始化

執(zhí)行koa()的時候初始化了一些很有用的東西,包括初始化一個空的中間件集合,基于Request赎离,Response,Context為原型端辱,生成實(shí)例等操作梁剔。Request和Response的屬性和方法委托到Context中也是在這一步進(jìn)行的圾浅。在這一步并沒有啟動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 環(huán)境憾朴,默認(rèn)為 NODE_ENV 或者 development
proxy 如果為 true,則解析 "Host" 的 header 域喷鸽,并支持 X-Forwarded-Host
subdomainOffset 默認(rèn)為2众雷,表示 .subdomains 所忽略的字符偏移量。

啟動Server

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

在執(zhí)行app.listen(1995)的時候做祝,啟動了一個server砾省,并且監(jiān)聽端口。
http.createServer接收一個函數(shù)作為參數(shù)混槐,每次服務(wù)器接收到請求都會執(zhí)行這個函數(shù)编兄,并傳入兩個參數(shù)(request和response,簡稱req和res)声登,那么現(xiàn)在重點(diǎn)在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,他的作用是把一個個不相干的中間件串聯(lián)在一起脯厨。

// 有3個中間件
this.middlewares = [function *m1() {}, function *m2() {}, function *m3() {}];
// 通過compose轉(zhuǎn)換
var middleware = compose(this.middlewares);
// 轉(zhuǎn)換后得到的middleware是這個樣子的
function *() {
yield *m1(m2(m3(noop())))
}

上述是V1的代碼铅祸,跟V2意思差不多。generator函數(shù)的特性是合武,第一次執(zhí)行并不會執(zhí)行函數(shù)里的代碼临梗,而是生成一個generator對象,這個對象有next稼跳,throw等方法盟庞。
這就造成了一個現(xiàn)象,每個中間件都會有一個參數(shù)岂贩,這個參數(shù)就是下一個中間件執(zhí)行后茫经,生成出來的generator對象,沒錯萎津,這就是大名鼎鼎的 next卸伞。
那compose是如何實(shí)現(xiàn)這樣的功能的呢?我們看一下代碼:

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

這里的邏輯就是:先把中間件從后往前依次執(zhí)行锉屈,并把每一個中間件執(zhí)行后得到的值賦值給變量next荤傲,當(dāng)下一次執(zhí)行中間件的時候(也就是執(zhí)行前一個中間件的時候),把next傳給第二個參數(shù)颈渊。這樣就保證前一個中間件的參數(shù)是下一個中間件生成的值遂黍,第一次執(zhí)行的時候next為underfined终佛。



compose(this.middleware)() = this.middleware[0](context, this.middleware[1](context, this.middleware[2](context, null)))
有一個問題,什么時候會出現(xiàn)i <= index的情況雾家?答案是當(dāng)一個中間件中兩次調(diào)用next時铃彰。比如,當(dāng)?shù)诙€中間件里兩次調(diào)用next芯咧,那執(zhí)行結(jié)果就變成了這樣牙捉。

可以看出,當(dāng)?shù)诙螆?zhí)行next時敬飒,index===i邪铲,就會拋出異常。

注意:一個中間件里是不能多次調(diào)用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);
}

創(chuàng)建上下文

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

對應(yīng)的源碼是:

/**
* 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其實(shí)就是app.createContext方法返回的完整版context带到。
又由于這段代碼的執(zhí)行時間是接受請求的時候,所以表明:
每一次接受到請求英染,都會為該請求生成一個新的上下文揽惹。
可以看看,經(jīng)過這一步處理后他們之間的關(guān)系是怎樣的税迷,可以用這個圖來表示:

從上圖中永丝,可以看到分別有五個箭頭指向ctx,表示ctx上包含5個屬性箭养,分別是request慕嚷,response,req毕泌,res喝检,app。request和response也分別有5個箭頭指向它們撼泛,所以也是同樣的邏輯挠说。


介紹一下這幾個概念:
ctx,就是上下文愿题,context 在每個 request 請求中被創(chuàng)建损俭,在中間件中作為接收器(receiver)來引用,或者通過 this 標(biāo)識符來引用:

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

node里有request和response兩個對象潘酗,分別有處理請求和響應(yīng)的API杆兵。在koa里,將這兩個對象封裝在了ctx里仔夺,可以通過ctx.req(=noderequest)和ctx.res(=node request)來使用琐脏。
Koa Request 對象(=ctx.request)是對 node 的 request 進(jìn)一步抽象和封裝,提供了日常 HTTP 服務(wù)器開發(fā)中一些有用的功能。
Koa Response 對象(=ctx.response)是對 node 的 response 進(jìn)一步抽象和封裝日裙,提供了日常 HTTP 服務(wù)器開發(fā)中一些有用的功能吹艇。
許多 context 的訪問器和方法為了便于訪問和調(diào)用,簡單的委托給他們的 ctx.request 和 ctx.response 所對應(yīng)的等價方法昂拂,比如說 ctx.type 和 ctx.length 代理了 response 對象中對應(yīng)的方法受神,ctx.path 和 ctx.method 代理了 request 對象中對應(yīng)的方法。
app是應(yīng)用實(shí)例引用格侯。
具體API看http://koa.bootcss.com/


錯誤監(jiān)視

onFinished(res, ctx.onerror);

這行代碼的作用是監(jiān)聽response路克,如果response有錯誤,會執(zhí)行ctx.onerror中的邏輯养交,設(shè)置response類型,狀態(tài)碼和錯誤信息等瓢宦。

執(zhí)行中間件

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

fn是compose(初始化中間件)執(zhí)行后的結(jié)果碎连,fn.call(ctx)執(zhí)行中間件邏輯,執(zhí)行成功則接著執(zhí)行response.call(ctx)驮履,否則進(jìn)行錯誤處理鱼辙。
請求的時候會經(jīng)過一次中間件,響應(yīng)的時候又會經(jīng)過一次中間件玫镐。


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

用一張圖表示如下:



由于每次接收請求倒戏,都會執(zhí)行callback,所以每次接收請求以下操作都會被執(zhí)行:

  • 初始化中間件
  • 生成新的上下文
  • 執(zhí)行中間件邏輯
  • 響應(yīng)請求或錯誤處理
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末恐似,一起剝皮案震驚了整個濱河市杜跷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌矫夷,老刑警劉巖葛闷,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異双藕,居然都是意外死亡淑趾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進(jìn)店門忧陪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扣泊,“玉大人,你說我怎么就攤上這事嘶摊⊙有罚” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵更卒,是天一觀的道長等孵。 經(jīng)常有香客問我,道長蹂空,這世上最難降的妖魔是什么俯萌? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任果录,我火速辦了婚禮,結(jié)果婚禮上咐熙,老公的妹妹穿的比我還像新娘弱恒。我一直安慰自己,他們只是感情好棋恼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布返弹。 她就那樣靜靜地躺著,像睡著了一般爪飘。 火紅的嫁衣襯著肌膚如雪义起。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天师崎,我揣著相機(jī)與錄音默终,去河邊找鬼。 笑死犁罩,一個胖子當(dāng)著我的面吹牛齐蔽,可吹牛的內(nèi)容都是我干的昌腰。 我是一名探鬼主播阀溶,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼伙窃,長吁一口氣:“原來是場噩夢啊……” “哼君丁!你這毒婦竟也來了仍秤?” 一聲冷哼從身側(cè)響起牍蜂,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嫩海,失蹤者是張志新(化名)和其女友劉穎早抠,沒想到半個月后递胧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸦做,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年谓着,在試婚紗的時候發(fā)現(xiàn)自己被綠了泼诱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡赊锚,死狀恐怖治筒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情舷蒲,我是刑警寧澤耸袜,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站牲平,受9級特大地震影響堤框,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一蜈抓、第九天 我趴在偏房一處隱蔽的房頂上張望启绰。 院中可真熱鬧,春花似錦沟使、人聲如沸委可。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽着倾。三九已至,卻和暖如春燕少,著一層夾襖步出監(jiān)牢的瞬間卡者,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工客们, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留虎眨,地道東北人。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓镶摘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親岳守。 傳聞我的和親對象是個殘疾皇子凄敢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容