koa 框架是基于 Node.js 下一代的 web server 框架, 舍棄了回調(diào)寫法, 提高了錯(cuò)誤處理效率, 而且其不綁定任何中間件, 核心代碼只提供優(yōu)雅輕量的函數(shù)庫(kù).
平時(shí)經(jīng)常使用到 koa 框架, 所以希望通過(guò)閱讀源碼學(xué)習(xí)其思想, 本文是基于 koa2 的源碼進(jìn)行分析.
koa 整體架構(gòu)
koa 框架的源碼結(jié)構(gòu)非常簡(jiǎn)單, 在 lib 文件夾下, 只有 4 個(gè)文件, 分別是application.js, context.js, request.js, response.js.
- application.js 是 koa 框架的入口文件;
- context.js 的作用是創(chuàng)建網(wǎng)絡(luò)請(qǐng)求的上下文對(duì)象;
- request.js 是用于包裝 koa 的 request 對(duì)象的;
- response.js則是用于包裝 koa 的 response 對(duì)象的.
我們這里使用 koa 框架建立一個(gè)簡(jiǎn)單的 node 服務(wù), 以此來(lái)逐步了解 koa 內(nèi)部機(jī)理.
const koa = require('koa');
?
const app = new koa();
?
app.use(async (ctx, next) {
ctx.body = 'Hello World';
});
?
app.listen(3000);
上面的代碼, 先生成了一個(gè) koa 對(duì)象, 然后通過(guò)使用 use 函數(shù)往 server 中添加中間件函數(shù), 最后使用 listen 函數(shù)進(jìn)行對(duì) 3000 端口的監(jiān)聽(tīng).
koa 源碼剖析
由上面的簡(jiǎn)單代碼, 我們會(huì)有幾個(gè)疑問(wèn): koa 對(duì)象中包含了些什么屬性與方法? use 函數(shù)對(duì)于中間件函數(shù)的處理是怎么樣的? listen 函數(shù)做了什么?
因此我們先來(lái)看一下 application.js 的源碼:
application.js
application.js 暴露了一個(gè) Application 類供我們使用, 也即是說(shuō), 我們 new 一個(gè) koa 對(duì)象實(shí)質(zhì)上就是新建一個(gè) Application 的實(shí)例對(duì)象. 而 Application 類是繼承于 EventEmitter (Node.js events 模塊)的, 所以我們?cè)?koa 實(shí)例對(duì)象上可以使用 on, emit 等方法進(jìn)行事件監(jiān)聽(tīng).
構(gòu)造函數(shù)
constructor() {
super(); // 因?yàn)槔^承于 EventEmitter, 這里需要調(diào)用 super
this.proxy = false; // 代理設(shè)置
this.middleware = []; // 存儲(chǔ)中間件的list
this.subdomainOffset = 2; // 子域名偏移設(shè)置
this.env = process.env.NODE_ENV || 'development'; // node 環(huán)境變量
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
可以看到在 constructor 函數(shù)中, 實(shí)例對(duì)象會(huì)初始化幾個(gè)重要的屬性,
- proxy 屬性是代理設(shè)置;
- middleware 屬性是中間件數(shù)組, 用于存儲(chǔ)中間件函數(shù)的;
- subdomainOffset 屬性是子域名偏移量設(shè)置;
- env 屬性保存 node 的環(huán)境變量 NODE_ENV 值;
- context, requets, response 則是 koa 自身的包裝的 context 對(duì)象, request 對(duì)象, response 對(duì)象.
這里特別講解一下 proxy 屬性與subdomainOffset 屬性. proxy 屬性值是 true 或者 false, 它的作用在于是否獲取真正的客戶端 ip 地址(詳細(xì)請(qǐng)看附錄的第一點(diǎn)). subdomainOffset 屬性會(huì)改變獲取 subdomain 時(shí)返回?cái)?shù)組的值, 比如 test.page.example.com 域名, 如果設(shè)置 subdomainOffset 為 2, 那么返回的數(shù)組值為 [“page”, “test”], 如果設(shè)置為 3, 那么返回?cái)?shù)組值為 [“test”].
app.use()與中間件
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3\. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
本文基于koa2,也就是async/await版本,所以關(guān)于generator 函數(shù)暫且不看。
所以在調(diào)用app.use()時(shí),也很簡(jiǎn)單,僅僅是把當(dāng)前中間件push進(jìn)中間件數(shù)組this.middleware。
所以, 所謂中間件函數(shù)的串聯(lián)其實(shí)就是通過(guò)數(shù)組來(lái)逐個(gè)執(zhí)行的, 至于 koa 是怎么利用 koa-compose 建立起核心的中間件機(jī)制的, 這里按下不表, 詳細(xì)請(qǐng)閱讀 理解 koa 中間件機(jī)制 博文.
listen 原理
listen 函數(shù)的原理其實(shí)很簡(jiǎn)單, 它實(shí)際上是一個(gè)縮寫的函數(shù), 它本質(zhì)上就是在內(nèi)部通過(guò) Node 原生的http 模塊建立起一個(gè) http server, 而這個(gè) http server 的回調(diào)函數(shù)使用的是 koa 中的 callback 函數(shù)的執(zhí)行結(jié)果(也就是callback函數(shù)return 的函數(shù)).
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
下面我們來(lái)看一下this.callback()函數(shù)。
callback() {
const fn = compose(this.middleware);
?
if (!this.listenerCount('error')) this.on('error', this.onerror);
?
// handleRequest 函數(shù)相當(dāng)于 http.creatServer 的回調(diào)函數(shù), 有 req, res 兩個(gè)參數(shù),
// 代表原生的 request, response 對(duì)象.
const handleRequest = (req, res) => {
// 每次接受一個(gè)新的請(qǐng)求就是生成一次全新的 context
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
?
return handleRequest;
}
?
?
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err); // 錯(cuò)誤處理
const handleResponse = () => respond(ctx); // 響應(yīng)處理
// 為 res 對(duì)象添加錯(cuò)誤處理響應(yīng), 當(dāng) res 響應(yīng)結(jié)束時(shí), 執(zhí)行 context 中的 onerror 函數(shù)
// (這里需要注意區(qū)分 context 與 koa 實(shí)例中的 onerror)
onFinished(res, onerror);
// 執(zhí)行中間件數(shù)組所有函數(shù), 并結(jié)束時(shí)調(diào)用 respond 函數(shù)
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
對(duì)于 this.createContext 函數(shù), 它的用于就是生成一個(gè)新的 context 對(duì)象并建立 koa 中 context, requets, response 屬性之間與原生 http 對(duì)象的關(guān)系的.
而 handleRequest 函數(shù)只是負(fù)責(zé)執(zhí)行中間件所有的函數(shù), 并在中間件函數(shù)執(zhí)行結(jié)束的時(shí)候調(diào)用 respond.
對(duì)于在 koa 中的 context 對(duì)象, request 對(duì)象, response 對(duì)象與 http 模塊原生的 req 與 res 之間的關(guān)系我并不打算陳列代碼, 下面我以圖解的形式來(lái)幫助閱讀:

request.js
request.js主要是對(duì)原生的 http 模塊的 requets 對(duì)象進(jìn)行封裝, 其實(shí)就是對(duì) request 對(duì)象某些屬性或方法通過(guò)重寫 getter/setter 函數(shù)進(jìn)行代理, 請(qǐng)看下面的圖進(jìn)行更好的理解:

內(nèi)容協(xié)商
TODO
response.js
同樣的, response.js 也是對(duì) http 模塊的 response 對(duì)象進(jìn)行封裝, 通過(guò)對(duì) response 對(duì)象的某些屬性或方法通過(guò)重寫 getter/setter 函數(shù)進(jìn)行代理, 請(qǐng)看下面的圖幫助理解:

context.js
分析了上面的 request 與 response, context 的分析更為簡(jiǎn)單了, context 的核心就是通過(guò) delegates 這一個(gè)庫(kù), 將 request, response 對(duì)象上的屬性方法代理到 context 對(duì)象上.
也就是說(shuō)例如 this.ctx.headersSent 相當(dāng)于 this.response.headersSent. request 對(duì)象與 response 對(duì)象的所有方法與屬性都能在 ctx 對(duì)象上找到. 這里我們來(lái)看一下 delegates 庫(kù)的屬性代理函數(shù)的片段, 借此理解一下 context 是如何代理 request 與 response 上的屬性與方法的:
delegate(proto, 'response')
.getter('headerSent');
Delegator.prototype.getter = function(name){
// this.proto 指向原型, 這里的 proto 就是上面的 proto, 也就是說(shuō) context 對(duì)象
var proto = this.proto;
// target 是指 'response' 字符串
var target = this.target;
// 將 name 加入到 delegator 實(shí)例對(duì)象的 getters 數(shù)組中
this.getters.push(name);
// 調(diào)用原生的 __defineGetter__ 方法進(jìn)行 getter 代理, 那么 proto[name] 就相當(dāng)于 proto[target][name]
// 而 context.response 就相當(dāng)于 response 對(duì)象
// 由此實(shí)現(xiàn)屬性代理
proto.__defineGetter__(name, function(){
return this[target][name];
});
?
return this;
};