Koa2原理詳解
1. Koa vs Express
Koa 是繼 Express 之后,Node的又一主流Web開發框架。相比于Express,Koa只保留了核心的中間件處理邏輯,去掉了路由,模板,以及其他一些功能。
另一方面,在中間件的處理過程中,Koa和Express也有著一定區別,看下面例子:
// http style
http.createServer((req, res) => {
// ...
})
// express style
app.use((req, res, next) => {
// ...
})
// koa style
app.use((ctx, next) => {
// ...
})</code></pre>
Node自帶的 http 模塊處理請求的時候,參數是一個 req 和 res ,分別為 http.IncomingMessage 和 http.ServerResponse 的實例。
Express對請求參數 req 和 res 的原型鏈進行了擴展,增強了 req 和 res 的行為。
而Koa并沒有改變 req 和 res ,而是通過 req 和 res 封裝了一個 ctx (context) 對象,進行后面的邏輯處理。
2. Koa基本組成
Koa源碼非常精簡,只有四個文件:
- application.js :Application(或Koa)負責管理中間件,以及處理請求
- context.js :Context維護了一個請求的上下文環境
- request.js :Request對 req 做了抽象和封裝
- response.js :Response對 res 做了抽象和封裝
3. Application
Application主要維護了中間件以及其它一些環境:
// application.js
module.exports = class Application extends Emitter {
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);
}
// ...
}</code></pre>
通過 app.use(fn) 可以將 fn 添加到中間件列表 this.middleware 中。
app.listen 方法源碼如下:
// application.js
listen() {
debug('listen');
const server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
}
首先會通過 this.callback 方法來返回一個函數作為 http.createServer 的回調函數,然后進行監聽。我們已經知道, http.createServer 的回調函數接收兩個參數: req 和 res ,下面來看 this.callback 的實現:
// application.js
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);
onFinished(res, ctx.onerror);
fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
};
}</code></pre>
首先是將所有的中間件通過 compose 組合成一個函數 fn ,然后返回 http.createServer 所需要的回調函數。于是我們可以看到,當服務器收到一個請求的時候,會使用 req 和 res 通過 this.createContext 方法來創建一個上下文環境 ctx ,然后使用 fn 來進行中間件的邏輯處理。
4. Context
通過上面的分析,我們已經可以大概得知Koa處理請求的過程:當請求到來的時候,會通過 req 和 res 來創建一個 context (ctx) ,然后執行中間件。
事實上,在創建 context 的時候,還會同時創建 request 和 response ,通過下圖可以比較直觀地看到所有這些對象之間的關系。

圖中:
- 最左邊一列表示每個文件的導出對象
- 中間一列表示每個Koa應用及其維護的屬性
- 右邊兩列表示對應每個請求所維護的一些列對象
- 黑色的線表示實例化
- 紅色的線表示原型鏈
- 藍色的線表示屬性
實際上, ctx 主要的功能是代理 request 和 response 的功能,提供了對 request 和 response 對象的便捷訪問能力。在源碼中,我們可以看到:
// context.js
delegate(proto, 'response')
.method('attachment')
// ...
.access('status')
// ...
.getter('writable');
delegate(proto, 'request')
.method('acceptsLanguages')
// ...
.access('querystring')
// ...
.getter('ip');</code></pre>
這里使用了 delegates 模塊來實現屬性訪問的代理。
簡單來說,通過 delegate(proto, 'response') ,當訪問 proto 的代理屬性的時候,實際上是在訪問 proto.response 的對應屬性。
5. Request & Response
Request對 req 進行了抽象和封裝,其中對于請求的url相關的處理如圖:
┌────────────────────────────────────────────────────────┐
│ href │
├────────────────────────────┬───────────────────────────┤
│ origin │ url / originalurl │
├──────────┬─────────────────┼──────────┬────────────────┤
│ protocol │ host │ path │ search │
├──────────├──────────┬──────┼──────────┼─┬──────────────┤
│ │ hostname │ port │ │?│ querystring │
│ ├──────────┼──────┤ ├─┼──────────────┤
│ │ │ │ │ │ │
" http: │ host.com : 8080 /p/a/t/h ? query=string │
│ │ │ │ │ │ │
└──────────┴──────────┴──────┴──────────┴─┴──────────────┘
Response對 res 進行了封裝和抽象,這里不做贅述。
來自:http://syaning.com/2016/11/08/koa2/