前段時間看了 koa 源碼,得益于 koa 良好抽象,不僅提供了簡潔的 api ,同時也使得源碼相當的簡潔和優雅。今天花點時間畫了一張 koa 源碼的結構圖來分析其源碼,在總結的同時,希望能夠幫到相關的同學。
注:源碼是基于 2.x 版本,源碼結構與 1.x 完全一致,代碼更加簡潔直觀一點。
基礎知識
任何用過 node 的人對下面的代碼都不會陌生,如下:
const http = require('http');
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});
server.listen(3000);
上面的代碼很簡單,http 的 createServer 方法創建了一個 http.Server
的實例,http.Sever
繼承于 EventEmitter
類。我們傳入的函數,其實是添加到了 server 的 response
事件中,相當于一種快捷方式。所以你也完全可以不傳入任何參數,然后手動去監聽 response
事件。代碼的最后將 server 綁定到 3000 端口,開始監聽所有來自 3000 端口的請求。
然后,我們看一下 response
事件的監聽函數,其函數接受兩個參數,分別是 req 和 res。其中 req 是一個可讀流,res 是一個可寫流。我們通過 req 獲取該請 http 請求的所有信息,同時將數據寫入到 res 來對該請求作出響應。
以上就是所有你需要知道的基礎知識,是不是很簡單?
koa 源碼解讀
分析源碼之前,先直觀看一下 koa 如何創建一個 server:
const Koa = require('koa');
const app = new Koa();
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
現在,我們由上面這段簡單的代碼開始深入到 koa 源碼,在分析源碼之前,我直接放上文章開頭提到的示意圖,以便我們結合示意圖進行分析。
構造函數
首先我們創建了 Koa 的實例 app,其構造函數十分簡單,如下:
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);
}
結合示意圖,這個代碼很簡單,其中 context 、request 和 response 就是 3 個字面量形式創建的簡單對象,上面封裝了一些列方法(其實絕大部分是屬性的賦值器(setter)和取值器(getter)),暫時不用管它們。就像示意圖所示,他們將作為 app 相應屬性的原型。
值得一提的是 Application 是繼承于 EventEmitter,如下:
class Application extends Emitter { //... }
中間件
接下來使用 use 的方法注冊一個中間件,其實就是簡單的 push 到自身的 mideware 這個數組中。如下:
use(fn) {
// 省略了一點點校驗參數性質的代碼
this.middleware.push(fn);
return this;
}
啟動應用
核心的代碼在 listen 這個方法,但是它做的事件也很簡單。我們已經知道,通過向 http.createServer 傳遞一個函數作為參數的形式來創建其的一個實例,其實 app.listen 里面就做了這個一個事情,源碼依舊十分簡單:
listen() {
const server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
}
唯一需要我們關注的就是這個 this.callback ,也是理解 koa 應用的核心所在。
this.callback() 執行的結果肯定是一個函數,根據我們基礎知識,這個函數無非就是根據 req 獲取信息,同時向 res 中寫入數據而已。
那么這個函數具體是怎么做的呢?首先,它基于 req 和 res 封裝出我們中間件所使用的 ctx 對象,再將 ctx 傳遞給中間件所組合成的一個嵌套函數。中間件組合的嵌套函數返回的是一個 Promise 的實例,等到這個組合函數執行完( resolve ),通過 ctx 中的信息(例如 ctx.body )想 res 中寫入數據,執行過程中出錯 (reject),這調用默認的錯誤處理函數。
原理還是很簡單,看一下代碼:
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
return (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
onFinished(res, onerror);
fn(ctx).then(() => respond(ctx)).catch(onerror);
};
}
就像我們分析的一樣,callback 首先會將我們的中間件組合成為一個嵌套函數供返回的函數執行時調用。createContext 根據 req 和 res 封裝中間件所需要的 ctx。onFinished 是確保一個流在關閉、完成和報錯時都會執行相應的回調函數。onerror 就是我們錯誤處理函數,最后分析。respond 就是我們根據 ctx 中的數據,然后集中的向 res 中寫入數據,響應 http 請求。代碼很簡單但是有點長,就不貼出來了。
現在核心是 createContext 是如何封裝出 ctx 的,直接看源碼,因為太簡單了:
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;
// 省略一點無關緊要的代碼
return context;
}
簡單的說就是創建了3個簡單的對象,并且將他們的原型指定為我們 app 中對應的對象。然后將原生的 req 和 res 賦值給相應的屬性,就完成了。
如我們示意圖,整個 Koa 的結構就完整了。
但是,ctx 上不是暴露出來很多屬性嗎?它們在哪?他們就在我們示意圖的最右邊,一開始我們略過的 3 個簡單對象。通過原型鏈的形式,我們 ctx.request 所能訪問屬性和方法絕大部分都在其對應的 request 這個簡單的對象上面。request 又是怎么封裝的呢?我只需要簡單的貼一點源碼,大家就秒懂了。
module.exports = {
//...
get method() {
return this.req.method;
},
set method(val) {
this.req.method = val;
}
//...
}
所以你訪問 ctx.request.xxx 屬性都是定義在右邊 resquest 這個簡單對象上的屬性的賦值器(setter)和取值器(getter)。
response 同理,也就不再贅述。
值得一提的是右邊 context 這個對象上面只提供了極少的方法,其他屬性都是簡單的代理到自身的 request 和 response 這兩個對象上。
錯誤處理
最后簡單的分析一下錯誤處理,由之前 callback 中的源碼我們可以看到,app 會默認注冊一個錯誤處理函數。
if (!this.listeners('error').length) this.on('error', this.onerror);
但是我們每次 http 請求的錯誤其實是交個 ctx.onerror 處理的:
const onerror = err => ctx.onerror(err);
onFinished(res, onerror);
fn(ctx).then(() => respond(ctx)).catch(onerror);
onFinished 是確保一個流在關閉、完成和報錯時都會執行相應的回調函數。ctx.onerror 這個函數在參數為空或者 null 的時候,直接返回,不會做任何操作。
if (null == err) return;
否則,則會觸發 app 產生一個錯誤事件。
this.app.emit('error', err, this);
然后如果判斷該請求處理依舊沒有結束,也就是 app 注冊的 onerror 事件沒有結束該請求,則會嘗試向客戶端產生一個 500 的錯誤。判斷方式:
if (this.headerSent || !this.writable) {
err.headerSent = true;
return;
}
總結起來,我們可以在不同的抽象層次上處理錯誤。比如,我們可以在頂層的中間件將所有中間件產生的錯誤捕獲并處理了,這樣錯誤就不會被上層捕獲。我們也可以覆蓋 ctx.onerror 的方式來捕獲所有的異常,而且可以不觸發 app 的 error 事件。最后我們當然也可以直接監聽 app 的 error 事件的方式來處理錯誤。
最后的話
如文章開頭所言,koa 恰當的抽象,不僅提供了簡潔實用的 api ,同時也使得源碼相當的簡潔和優雅。雖然文章標題為看完 koa 源碼,但實際上并沒有對 response 和 request 封裝的細枝末節進行詳細的闡述,如果你看懂了這邊文章,再去看源碼應該就得心應手了。
最后如果闡述不清楚,或者不恰到的地方歡迎反饋。