Koa 2.x
版本是當下最流行的 NodeJS 框架,同時社區涌現出一大批圍繞 Koa 2.x
的中間件以及基於 Koa 2.x
封裝的企業級框架,如 egg.js
,然而 Koa
自己的代碼卻很是精簡,精簡到全部文件的代碼去掉註釋後還不足 2000
行,本篇就圍繞着這 2000
行不到的代碼抽出核心邏輯進行分析,並壓縮成一版只有 200
行不到的簡易版 Koa
。node
在下面的內容中,咱們將對 Koa
所使用的功能由簡入深的分析,首先會給出使用案例,而後根據使用方式,分析實現原理,最後對分析的功能進行封裝,封裝過程會從零開始並一步一步完善,代碼也是從少到多,會完整的看到一個簡版 Koa
誕生的過程,再此以前咱們打開 Koa
源碼地址。git
經過上面對 Koa
源碼目錄的截圖,發現只有 4
個核心文件,爲了方便理解,封裝簡版 Koa
的文件目錄結構也將嚴格與源碼同步。github
在引入 Koa
時咱們須要建立一個 Koa
的實例,而啓動服務是經過 listen
監聽一個端口號實現的,代碼以下。編程
const Koa = require("koa");
const app = new Koa();
app.listen(3000, () => {
console.log("server start 3000");
});複製代碼
經過使用咱們能夠分析出 Koa
導出的應該是一個類,或者構造函數,鑑於 Koa
誕生的時間以及基於 node v7.6.0
以上版本的狀況來分析,正是 ES6
開始 「橫行霸道」 的時候,因此推測 Koa
導出的應該是一個類,打開源碼一看,果真如此,因此咱們也經過 class
的方式來實現。json
而從啓動服務的方式上看,app.listen
的調用方式與原生 http
模塊提供的 server.listen
幾乎相同,咱們分析,listen
方法應該是對原生 http
模塊的一個封裝,啓動服務的本質仍是靠 http
模塊來實現的。redux
const http = require("http");
class Koa {
handleRequest(req, res) {
// 請求回調
}
listen(...args) {
// 建立服務
let server = http.createServer(this.handleRequest.bind(this));
// 啓動服務
server.listen(...args);
}
}
module.exports = Koa;複製代碼
上面的代碼初步實現了咱們上面分析出的需求,爲了防止代碼冗餘,咱們將建立服務的回調抽取成一個 handleRequest
的實例方法,內部的邏輯在後面完善,如今能夠建立這個 Koa
類的實例,經過調用實例的 listen
方法啓動一個服務器。數組
Koa
還有一個很重要的特性,就是它的 ctx
上下文對象,咱們能夠調用 ctx
的 request
和 response
屬性獲取原 req
和 res
的屬性和方法,也在 ctx
上增長了一些原生沒有的屬性和方法,總之 ctx
給咱們要操做的屬性和方法提供了多種調用方式,使用案例以下。promise
const Koa = require("koa");
const app = new Koa();
app.use((ctx, next) => {
// 原生的 req 對象的 url 屬性
console.log(ctx.req.url);
console.log(ctx.request.req.url);
console.log(ctx.response.req.url);
// Koa 擴展的 url
console.log(ctx.url);
console.log(ctx.request.req.url);
// 設置狀態碼和響應內容
ctx.response.status = 200;
ctx.body = "Hello World";
});
app.listen(3000, () => {
console.log("server start 3000");
});複製代碼
從上面咱們能夠看出,ctx
爲 use
方法的第一個參數,request
和 response
是 ctx
新增的,而經過這兩個屬性又均可以獲取原生的 req
和 res
屬性,ctx
自己也能夠獲取到原生的 req
和 res
,咱們能夠分析出,ctx
是對這些屬性作了一個集成,或者說特殊處理。瀏覽器
源碼的文件目錄中正好有與 request
、response
名字相對應的文件,而且還有 context
名字的文件,咱們其實能夠分析出這三個文件就是用於封裝 ctx
上下文對象使用的,而封裝 ctx
中也會用到 req
和 res
,因此核心邏輯應該在 handleRequest
中實現。bash
在使用案例中 ctx
是做爲 use
方法中回調函數的參數,因此咱們分析應該有一個數組統一管理調用 use
後傳入的函數,Koa
應該有一個屬性,值爲數組,用來存儲這些函數,下面是實現代碼。
const http = require("http");
// ***************************** 如下爲新增代碼 *****************************
const context = require("./context");
const request = require("./request");
const response = require("./response");
// ***************************** 以上爲新增代碼 *****************************
class Koa {
// ***************************** 如下爲新增代碼 *****************************
contructor() {
// 存儲中間件
this.middlewares = [];
// 爲了防止經過 this 修改屬性而致使影響原引入文件的導出對象,作一個繼承
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
// 將傳給 use 的函數存入數組中
this.middlewares.push(fn);
}
createContext(req, res) {
// 或取定義的上下文
let ctx = this.context;
// 增長 request 和 response
ctx.request = this.request;
ctx.response = this.response;
// 讓 ctx、request、response 都具備原生的 req 和 res
ctx.req = ctx.request.req = ctx.response.req = req;
ctx.res = ctx.response.res = ctx.request.res = res;
// 返回上下文對象
return ctx;
}
// ***************************** 以上爲新增代碼 *****************************
handleRequest(req, res) {
// 建立 ctx 上下文對象
let ctx = this.createContext(req, res);
}
listen(...args) {
// 建立服務
let server = http.createServer(this.handleRequest.bind(this));
// 啓動服務
server.listen(...args);
}
}
module.exports = Koa;複製代碼
首先,給實例建立了三個屬性 context
、request
和 response
分別繼承了 context.js
、request.js
和 response.js
導出的對象,之因此這麼作而不是直接賦值是防止操做實例屬性時 「污染」 原對象,而獲取原模塊導出對象的屬性能夠經過原型鏈進行查找,並不影響取值。
其次,給實例掛載了 middlewares
屬性,值爲數組,爲了存儲 use
方法調用時傳入的函數,在 handleRequest
把建立 ctx
屬性及引用的過程單獨抽取成了 createContext
方法,並在 handleRequest
中調用,返回值爲建立好的 ctx
對象,而在 createContext
中咱們根據案例中的規則構建了 ctx
的屬性相關的各類引用關係。
上面構建的屬性中,全部經過訪問原生 req
或 res
的屬性都能獲取到,反之則是 undefined
,這就須要咱們去構建 request.js
。
const url = require("url");
// 給 url 和 path 添加 getter
const request = {
get url() {
return this.req.url;
},
get path() {
return url.parse(this.req.url).pathname;
}
};
module.exports = request;複製代碼
上面咱們只構造了兩個屬性 url
和 path
,咱們知道 url
是原生所自帶的屬性,咱們在使用 ctx.request.url
獲取是經過 request
對象設置的 getter
,將 ctx.request.req.url
的值返回了。
path
是原生 req
所沒有的屬性,但倒是經過原生 req
的 url
屬性和 url
模塊共同構建出來的,因此咱們一樣用了給 request
對象設置 getter
的方式獲取 req
的 url
屬性,並使用 url
模塊將轉換對象中的 pathname
返回,此時就能夠經過 ctx.request.path
來獲取訪問路徑,至於源碼中咱們沒有處理的 req
屬性都是經過這樣的方式創建的引用關係。
Koa
中 response
對象的真正做用是給客戶端進行響應,使用時是經過訪問屬性獲取,並經過從新賦值實現響應,可是如今 response
獲取的屬性都是 undefined
,咱們這裏先無論響應給瀏覽器的問題,首先要讓 response
下的某個屬性有值才行,下面咱們來實現 response.js
。
// 給 body 和 status 添加 getter 和 setter
const response = {
get body() {
return this._body;
},
set body(val) {
// 只要給 body 賦值就表明響應成功
this.status = 200;
this._body = val;
},
get status() {
return this.res.statusCode;
},
set status(val) {
this.res.statusCode = val;
}
};
module.exports = response;複製代碼
這裏選擇了 Koa
在使用時,response
對象上比較重要的兩個屬性進行處理,由於這兩個屬性是服務器響應客戶端所必須的,並模仿了 request.js
的方式給 body
和 status
設置了 getter
,不一樣的是響應瀏覽器所作的實際上是賦值操做,因此又給這兩個屬性添加了 setter
,對於 status
來講,直接操做原生 res
對象的 statusCode
屬性便可,由於同爲賦值操做。
還有一點,響應是經過給 body
賦值實現,咱們認爲只要觸發了 body
的 setter
就成功響應,因此在 body
的 getter
中將響應狀態碼設置爲 200
,至於 body
賦值是如何實現響應的,放在後面再說。
上面實現了經過 request
和 response
對屬性的操做,Koa
雖然給咱們提供了多樣的屬性操做方式,但因爲咱們程序猿(媛)們都很 「懶」,幾乎沒有人會在開發的時候願意多寫代碼,大部分狀況都是經過 ctx
直接操做 request
和 response
上的屬性,這就是咱們如今的問題所在,這些屬性經過 ctx
訪問不到。
咱們須要給 ctx
對象作一個代理,讓 ctx
能夠訪問到 request
和 response
上的屬性,這個場景何曾相識,不正是 Vue
建立實例時,將傳入參數對象 options
的 data
屬性代理給實例自己的場景嗎,既然如此,咱們也經過類似的方式實現,還記得上面引入的 context
模塊做爲實例的 context
屬性所繼承的對象,而剩下的最後一個核心文件 context.js
正是用來作這件事的,代碼以下。
const proto = {};
// 將傳入對象屬性代理給 ctx
function defineGetter(property, key) {
proto.__defineGetter__(key, function () {
return this[property][key];
});
}
// 設置 ctx 值時直接操做傳入對象的屬性
function defineSetter(property, key) {
proto.__defineSetter__(key, function (val) {
this[property][key] = val;
});
}
// 將 request 的 url 和 path 代理給 ctx
defineGetter("request", "url");
defineGetter("request", "path");
// 將 response 的 body 和 status 代理給 ctx
defineGetter("response", "body");
defineSetter("response", "body");
defineGetter("response", "status");
defineSetter("response", "status");
module.exports = proto;複製代碼
在 Vue
中是使用 Object.defineProperty
來時實現的代理,而在 Koa
源碼中藉助了 delegate
第三方模塊來實現的,並在添加代理時鏈式調用了 delegate
封裝的方法,咱們並無直接使用 delegate
模塊,而是將 delegate
內部的核心邏輯抽取出來在 context.js
中直接編寫,這樣方便你們理解原理,也能夠清楚的知道是如何實現代理的。
咱們封裝了兩個方法 defineGetter
和 defineSetter
分別來實現取值和設置值時,將傳入的屬性(第二個參數)代理給傳入的對象(第一個參數),函數內是經過 Object.prototype.__defineGetter__
和 Object.prototype.__defineSetter__
實現的,點擊方法名可查看官方 API。
如今已經實現了 ctx
上下文對象的建立,可是會發現咱們封裝 ctx
以前所寫的案例 use
回調中的代碼並不能執行,也不會報錯,根本緣由是 use
方法內傳入的函數沒有調用,在使用 Koa
的過程當中會發現,咱們每每使用多個 use
,而且傳入 use
的回調函數除了 ctx
還有第二個參數 next
,而這個 next
也是一個函數,調用 next
則執行下一個 use
中的回調函數,不然就會 「卡住」,這種執行機制被取名爲 「洋蔥模型」,而這些被執行的函數被稱爲 「中間件」,下面咱們就來分析這個 「洋蔥模型」 並實現中間件的串行。
下面來看看錶述洋蔥模型的一個經典案例,結果彷佛讓人匪夷所思,一時很難想到緣由,不着急先看了再說。
const Koa = require("koa");
const app = new Koa();
app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});
app.use((ctx, next) => {
console.log(5);
next();
console.log(6);
});
app.listen(3000, () => {
console.log("server start 3000");
});
// 1
// 3
// 5
// 6
// 4 複製代碼
// 2根據上面的執行特性咱們不妨來分析如下,咱們知道 use
方法執行時實際上是把傳入的回調函數放入了實例的 middlewares
數組中,而執行結果打印了 1
說明第一個回調函數被執行了,接着又打印了 2
說明第二個回調函數被執行了,根據上面的代碼咱們能夠大膽的猜測,第一個回調函數調用的 next
確定是一個函數,可能就是下一個回調函數,或者是 next
函數中執行了下一個回調函數,這樣根據函數調用棧先進後出的原則,會在 next
執行完畢,即出棧後,繼續執行上一個回調函數的代碼。
在實現中間件串行以前須要補充一點,中間件函數內調用 next
時,前面的代碼出現異步,則會繼續向下執行,等到異步執行結束後要執行的代碼插入到同步代碼中,這會致使執行順序錯亂,因此在官方推薦中告訴咱們任何遇到異步的操做前都須要使用 await
進行等待(包括 next
,由於下一個中間件中可能包含異步操做),這也間接的說明了傳入 use
的回調函數只要有異步代碼須要 await
,因此應該是 async
函數,而瞭解 ES7
特性 async/await
的咱們來講,必定能分析出 next
返回的應該是一個 Promise 實例,下面是咱們在以前 application.js
基礎上的實現。
const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");
class Koa {
contructor() {
// 存儲中間件
this.middlewares = [];
// 爲了防止經過 this 修改屬性而致使影響原引入文件的導出對象,作一個繼承
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
// 將傳給 use 的函數存入數組中
this.middlewares.push(fn);
}
createContext(req, res) {
// 或取定義的上下文
let ctx = this.context;
// 增長 request 和 response
ctx.request = this.request;
ctx.response = this.response;
// 讓 ctx、request、response 都具備原生的 req 和 res
ctx.req = ctx.request.req = ctx.response.req = req;
ctx.res = ctx.response.res = ctx.request.res = res;
// 返回上下文對象
return ctx;
}
// ***************************** 如下爲新增代碼 *****************************
compose(ctx, middles) {
// 建立一個遞歸函數,參數爲存儲中間件的索引,從 0 開始
function dispatch(index) {
// 在全部中間件執行以後給 compose 返回一個 Promise(兼容一箇中間件都沒寫的狀況)
if (index === middles.length) return Promise.resolve();
// 取出第 index 箇中間件函數
const route = middles[index];
// 爲了兼容中間件傳入的函數不是 async,必定要包裝成一個 Promise
return Promise.resolve(route(ctx, () => dispatch(index + 1)));
}
return dispatch(0); // 默認執行一次
}
// ***************************** 以上爲新增代碼 *****************************
handleRequest(req, res) {
// 建立 ctx 上下文對象
let ctx = this.createContext(req, res);
// ***************************** 如下爲新增代碼 *****************************
// 執行 compose 將中間件組合在一塊兒
this.compose(ctx, this.middlewares);
// ***************************** 以上爲新增代碼 *****************************
}
listen(...args) {
// 建立服務
let server = http.createServer(this.handleRequest.bind(this));
// 啓動服務
server.listen(...args);
}
}
module.exports = Koa;複製代碼
仔細想一想咱們其實在利用循環執行每個 middlewares
中的函數,並且須要把下一個中間件函數的執行做爲函數體的代碼包裝一層成爲新的函數,並做爲參數 next
傳入,那麼在上一個中間件函數內部調用 next
就至關於先執行了下一個中間件函數,而下一個中間件函數內部調用 next
,又先執行了下一個的下一個中間件函數,依次類推。
直到執行到最後一箇中間件函數,調用了 next
,可是 middlewares
中已經沒有下一個中間件函數了,這也是爲何咱們要給下一個中間件函數外包了一層函數而不是直接將中間件函數傳入的緣由之一(另外一個緣由是解決傳參問題,由於在執行時還要傳入下一個中間件函數),可是防止遞歸 「死循環」,要配合一個終止條件,即指向 middlewares
索引的變量等於了 middlewares
的長度,最後只是至關於執行了一個只有一條判斷語句的函數就 return
的函數,而並無報錯。
在這整個過程當中若是有任意一個 next
沒有被調用,就不會向下執行其餘的中間件函數,這樣就 「卡住了」,徹底符合 Koa
中間件的執行規則,而 await
事後也就是下一個中間件優先執行完成,則會繼續執行當前中間件 next
調用下面的代碼,這也就是 一、三、五、六、四、2
的由來。
爲了實現所描述的執行過程,將全部中間件串行的邏輯抽出了一個 compose
方法,可是咱們沒有使用普通的循環,而是使用遞歸實現的,首先在 compose
建立 dispatch
遞歸函數,參數爲當前數組函數的索引,初始值爲 0
,函數邏輯是先取出第一個函數執行,並傳入一個回調函數參數,回調函數參數中遞歸 dispatch
,參數 +1
,這樣就會將整個中間件串行起來了。
可是上面的串行也只是同步串行,若是某個中間件內部須要等待異步,則調用得 next
函數必須返回一個 Promise,有些中間件沒有執行異步,則不須要 async
函數,也不會返回 Promise,而 Koa
規定只要遇到 next
就須要等待,則將取出每個中間件函數執行後的結果使用 Promise.resolve
強行包裝成一個成功態的 Promise,就對異步進行了兼容。
咱們最後也但願 compose
返回一個 Promise 方便執行一些只有在中間件都執行後纔會執行的邏輯,每次串行最後執行的都是一個只有一條判斷邏輯就 return
了的函數(包含一箇中間件也沒有的狀況),此時 compose
返回了 undefined
,沒法調用 then
方法,爲了兼容這種狀況也強行的使用相同的 「招數」,在判斷條件的 return
關鍵字後面加上了 Promise.resolve()
,直接返回了一個成功態的 Promise。
next
的時候使用 await
等待,即便執行的 next
真的存在異步,也不是非 await
不可,咱們徹底可使用 return
來代替 await
,惟一的區別就是 next
調用後,下面的代碼不會再執行了,類比 「洋蔥模型」,形象地說就是 「下去了就上不來了」,這個徹底能夠根據咱們的使用須要而定,若是 next
後面再也不有任何邏輯,徹底可使用 return
替代。
在對 ctx
實現屬性代理後,咱們經過 ctx.body
從新賦值其實只是改變了 response.js
導出對象的 _body
屬性,而並無實現真正的響應,看下面這個 Koa
的例子。
const Koa = require("koa");
const fs = require("fs");
const app = new Koa();
app.use(async (ctx, next) => {
ctx.body = "hello";
await next();
});
app.use(async (ctx, next) => {
ctx.body = fs.createReadStream("1.txt");
ctx.body = await new Promise((resolve, reject) => {
setTimeout(() => resolve("panda"), 3000);
});
});
app.listen(3000, () => {
console.log("server start 3000"); 複製代碼
});其實最後響應給客戶端的值是 panda
,正常在最後一箇中間件執行後,因爲異步定時器的代碼沒有執行完,ctx.body
最後的值應該是 1.txt
的可讀流,這與客戶端接收到的值相違背,經過這個猜測上的差別咱們應該知道,compose
在串行執行中間件後爲何要返回一個 Promise 了,由於最後執行的只有判斷語句的函數會等待咱們例子中最後一個 use
傳入的中間件函數執行完畢調用,也就是說在執行 compose
返回值的 then
時,ctx.body
的值已是 panda
了。
const http = require("http");
// ***************************** 如下爲新增代碼 *****************************
const Stream = require("stream");
// ***************************** 以上爲新增代碼 *****************************
const context = require("./context");
const request = require("./request");
const response = require("./response");
class Koa {
contructor() {
// 存儲中間件
this.middlewares = [];
// 爲了防止經過 this 修改屬性而致使影響原引入文件的導出對象,作一個繼承
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
// 將傳給 use 的函數存入數組中
this.middlewares.push(fn);
}
createContext(req, res) {
// 或取定義的上下文
let ctx = this.context;
// 增長 request 和 response
ctx.request = this.request;
ctx.response = this.response;
// 讓 ctx、request、response 都具備原生的 req 和 res
ctx.req = ctx.request.req = ctx.response.req = req;
ctx.res = ctx.response.res = ctx.request.res = res;
// 返回上下文對象
return ctx;
}
compose(ctx, middles) {
// 建立一個遞歸函數,參數爲存儲中間件的索引,從 0 開始
function dispatch(index) {
// 在全部中間件執行以後給 compose 返回一個 Promise(兼容一箇中間件都沒寫的狀況)
if (index === middles.length) return Promise.resolve();
// 取出第 index 箇中間件函數
const route = middles[index];
// 爲了兼容中間件傳入的函數不是 async,必定要包裝成一個 Promise
return Promise.resolve(route(ctx, () => dispatch(index + 1)));
}
return dispatch(0); // 默認執行一次
}
handleRequest(req, res) {
// 建立 ctx 上下文對象
let ctx = this.createContext(req, res);
// ***************************** 如下爲修改代碼 *****************************
// 設置默認狀態碼(Koa 規定),必須在調用中間件以前
ctx.status = 404;
// 執行 compose 將中間件組合在一塊兒
this.compose(ctx, this.middlewares).then(() => {
// 獲取最後 body 的值
let body = ctx.body;
// 檢測 ctx.body 的類型,並使用對應的方式將值響應給瀏覽器
if (Buffer.isBuffer(body) || typeof body === "string") {
// 處理 Buffer 類型的數據
res.setHeader("Content-Type", "text/plain;charset=utf8");
res.end(body);
} else if (typeof body === "object") {
// 處理對象類型
res.setHeader("Content-Type", "application/json;charset=utf8");
res.end(JSON.stringify(body));
} else if (body instanceof Stream) {
// 處理流類型的數據
body.pipe(res);
} else {
res.end("Not Found");
}
});
// ***************************** 以上爲修改代碼 *****************************
}
listen(...args) {
// 建立服務
let server = http.createServer(this.handleRequest.bind(this));
// 啓動服務
server.listen(...args);
}
}
module.exports = Koa;複製代碼
處理 response
時,在 body
的 setter
中將狀態碼設置爲了 200
,就是說須要設置 ctx.body
去觸發 setter
讓響應成功,若是沒有給 ctx.body
設置任何值,默認應該是無響應的,在官方文檔也有默認狀態碼爲 404
的明確說明,因此在 handleRequest
把狀態碼設置爲了 404
,但必須在 compose
執行以前才叫默認狀態碼,由於中間件中可能會操做 ctx.body
,從新設置狀態碼。
在 comose
的 then
中,也就是在全部中間件執行後,咱們取出 ctx.body
的值,即爲最後生效的響應值,對該值進行了數據類型驗證,如 Buffer、字符串、對象和流,並分別用不一樣的方式處理了響應,但本質都是調用的原生 res
對象的 end
方法。
在上面的邏輯當中咱們實現了不少 Koa
的核心邏輯,可是隻考慮了順利執行的狀況,並無考慮若是中間件中代碼執行出現錯誤的問題,以下面案例。
const Koa = require("koa");
const app = new Koa();
app.use((ctx, next) => {
// 拋出異常
throw new Error("Error");
});
// 添加 error 監聽
app.on("error", err => {
console.log(err);
});
app.listen(3000, () => {
console.log("server start 3000");
});複製代碼
咱們之因此讓 compose
方法在執行全部中間件後返回一個 Promise 還有一個更重要的意義,由於在 Promise 鏈式調用中,只要其中任何一個環節出現代碼執行錯誤或拋出異常,都會直接執行出現錯誤的 then
方法中錯誤的回調或者最後的 catch
方法,對於 Koa
中間件的串行而言,最後一個 then
調用 catch
方法就是 compose
的返回值調用 then
後繼續調用的 catch
,catch
內能夠捕獲到任意一箇中間件執行時出現的錯誤。
const http = require("http");
const Stream = require("stream");
// ***************************** 如下爲新增代碼 *****************************
const EventEmitter = require("events");
const httpServer = require("_http_server");
// ***************************** 以上爲新增代碼 *****************************
const context = require("./context");
const request = require("./request");
const response = require("./response");
// ***************************** 如下爲修改代碼 *****************************
// 繼承 EventEmitter 後能夠用建立的實例 app 添加 error 監聽,能夠經過 emit 觸發監聽
class Koa extends EventEmitter {
contructor() {
supper();
// ***************************** 以上爲修改代碼 *****************************
// 存儲中間件
this.middlewares = [];
// 爲了防止經過 this 修改屬性而致使影響原引入文件的導出對象,作一個繼承
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
// 將傳給 use 的函數存入數組中
this.middlewares.push(fn);
}
createContext(req, res) {
// 或取定義的上下文
let ctx = this.context;
// 增長 request 和 response
ctx.request = this.request;
ctx.response = this.response;
// 讓 ctx、request、response 都具備原生的 req 和 res
ctx.req = ctx.request.req = ctx.response.req = req;
ctx.res = ctx.response.res = ctx.request.res = res;
// 返回上下文對象
return ctx;
}
compose(ctx, middles) {
// 建立一個遞歸函數,參數爲存儲中間件的索引,從 0 開始
function dispatch(index) {
// 在全部中間件執行以後給 compose 返回一個 Promise(兼容一箇中間件都沒寫的狀況)
if (index === middles.length) return Promise.resolve();
// 取出第 index 箇中間件函數
const route = middles[index];
// 爲了兼容中間件傳入的函數不是 async,必定要包裝成一個 Promise
return Promise.resolve(route(ctx, () => dispatch(index + 1)));
}
return dispatch(0); // 默認執行一次
}
handleRequest(req, res) {
// 建立 ctx 上下文對象
let ctx = this.createContext(req, res);
// 設置默認狀態碼(Koa 規定),必須在調用中間件以前
ctx.status = 404;
// 執行 compose 將中間件組合在一塊兒
this.compose(ctx, this.middlewares).then(() => {
// 獲取最後 body 的值
let body = ctx.body;
// 檢測 ctx.body 的類型,並使用對應的方式將值響應給瀏覽器
if (Buffer.isBuffer(body) || typeof body === "string") {
// 處理 Buffer 類型的數據
res.setHeader("Content-Type", "text/plain;charset=utf8");
res.end(body);
} else if (typeof body === "object") {
// 處理對象類型
res.setHeader("Content-Type", "application/json;charset=utf8");
res.end(JSON.stringify(body));
} else if (body instanceof Stream) {
// 處理流類型的數據
body.pipe(res);
} else {
res.end("Not Found");
}
// ***************************** 如下爲修改代碼 *****************************
}).catch(err => {
// 執行 error 事件
this.emit("error", err);
// 設置 500 狀態碼
ctx.status = 500;
// 返回狀態碼對應的信息響應瀏覽器
res.end(httpServer.STATUS_CODES[ctx.status]);
});
// ***************************** 以上爲修改代碼 *****************************
}
listen(...args) {
// 建立服務
let server = http.createServer(this.handleRequest.bind(this));
// 啓動服務
server.listen(...args);
}
}
module.exports = Koa;複製代碼
在使用的案例當中,使用 app
(即 Koa
建立的實例)監聽了一個 error
事件,當中間件執行錯誤時會觸發該監聽的回調,這讓咱們想起了 NodeJS 中一個重要的核心模塊 events
,這個模塊幫咱們提供了一個事件機制,經過 on
方法添加監聽,經過 emit
觸發監聽,因此咱們引入了 events
,並讓 Koa
類繼承了 events
導入的 EventEmitter
類,此時 Koa
的實例就可使用 EventEmitter
原型對象上的 on
和 emit
方法。
在 compose
執行後調用的 catch
中,經過實例調用了 emit
,並傳入了事件類型 error
和錯誤對象,這樣就是實現了中間件的錯誤監聽,只要中間件執行出錯,就會執行案例中錯誤監聽的回調。
在上面咱們實現了 Koa
大部分經常使用功能的核心邏輯,但還有一點美中不足,就是咱們引入本身的簡易版 Koa
時,默認會查找 koa
路徑下的 index.js
,想要執行咱們的 Koa
必需要使用路徑找到 application.js
,代碼以下。
const Koa = require("./koa/application");複製代碼
const Koa = require("./koa");複製代碼
咱們更但願像直接引入指定 koa
文件夾,就能夠找到 application.js
文件並執行,這就須要咱們在 koa
文件夾建立 package.json
文件,並在動一點小小的 「手腳」 以下。
{
.
.
.
"main": "./application.js",
.
.
.
}複製代碼
在文章最後一節送給你們一張 Koa
執行的原理圖,這張圖片是準備寫這篇文章時在 Google 上發現的,以爲把 Koa
的整個流程表達的很是清楚,因此這裏拿來幫助你們理解 Koa
框架的原理和執行過程。
之因此沒有在文章開篇放上這張圖是由於以爲在徹底沒有了解過 Koa
的原理以前,可能有一部分小夥伴看這張圖會懵,會打消學習的積極性,由於本篇的目的就是帶着你們從零到有的,一步一步實現簡易版 Koa
,梳理 Koa
的核心邏輯,若是你已經看到了這裏,是否是以爲這張圖出現的不早不晚,剛恰好。
最後仍是在這裏作一個總結,在 Koa
中主要的部分有 listen
建立服務器、封裝上下文對象 ctx
並代理屬性、use
方法添加中間件、compose
串行執行中間、讓 Koa
繼承 EventEmitter
實現錯誤監聽,而我我的以爲最重要的就是 compose
,它是一個事件串行機制,也是實現 「洋蔥模型」 的核心,現在 compose
已經再也不只是一個方法名,而是一種編程思想,用於將多個程序串行在一塊兒,或同步,或異步,在 Koa
中自沒必要多說,由於你們已經見識過了,compose
在 React
中也起着串聯中間件的做用,如串聯 promise
、redux-thunk
、logger
等,在 Webpack
源碼依賴的核心模塊 tapable
中也有所應用,在咱們的學習過程當中,這樣優秀的編程思想是應該重點吸取的。