- 本文 github 地址: https://github.com/HCThink/h-blog/blob/master/source/koa2/readme.md
- github 首頁(star+watch,一手動態直達): https://github.com/HCThink/h-blog
- 掘金 link , 掘金 專欄
- segmentfault 主頁
原創禁止私自轉載
koa2
優秀的下一代 web 開發框架。
Koa 應用程序不是 HTTP 服務器的1對1展現。 可以將一個或多個 Koa 應用程序安裝在一起以形成具有單個HTTP服務器的更大應用程序。
基礎使用
快速搭建簡易 koa server 服務
koa 搭建一個服務還是很簡單的, 主要代碼如下, 完整代碼如下. 切到主目錄下,
- 安裝依賴:
yarn
- 執行入口:
yarn start
import Koa from 'koa';
import https from 'https';
import open from 'open';
const Log = console.log;
const App = new Koa();
App.use(async (ctx, next) => {
ctx.body = 'Hello World';
Log('mid1 start...');
await next();
Log('mid1 end...');
});
App.use(async (ctx, next) => {
debugger;
Log('mid2 start...');
await next();
Log('mid2 end...');
});
App.use((ctx, next) => {
Log('mid3...');
});
// 服務監聽: 兩種方式。
App.listen(3000); // 語法糖
// http.createServer(app.callback()).listen(3000);
https.createServer(App.callback()).listen(3001);
open('http://localhost:3000');
// 如下爲執行順序, 實際上 http 會握手,所以輸出多次
// 如下執行特徵也就是洋蔥圈, 實際上熟悉 async、await 則不會比較意外。
// mid1 start...
// mid2 start...
// mid3...
// mid2 end...
// mid1 end...
koa2特性
- 封裝並增強 node http server[request, response],簡單易容。
- 洋蔥圈處理模型。
- 基於 async/await 的靈活強大的中間件機制。
- 通過委託使得 api 在使用上更加便捷易用。
api
參考官網提供的基本 api ,不在贅述: https://koa.bootcss.com/
部分 api 實現,參考: 源碼分析
常用 api
- app.listen: 服務端口監聽
- app.callback: 返回適用於 http.createServer() 方法的回調函數來處理請求。你也可以使用此回調函數將 koa 應用程序掛載到 Connect/Express 應用程序中。
- app.use(function): 掛載中間件的主要方法。
核心對象
context
Koa Context 將 node 的 request 和 response 對象封裝到單個對象中,爲編寫 Web 應用程序和 API 提供了許多有用的方法。 這些操作在 HTTP 服務器開發中頻繁使用,它們被添加到此級別而不是更高級別的框架,這將強制中間件重新實現此通用功能。__每個__ 請求都將創建一個 Context,並在中間件中作爲接收器引用,或者 ctx 標識符。
- ctx.res request
- ctx.req: response
- ctx.request: koa request tool
- ctx.response: koa response tool
- ctx.cookies
- ctx.request.accepts(types): type 值可能是一個或多個 mime 類型的字符串,如 application/json,擴展名稱如 json,或數組 ["json", "html", "text/plain"]。
- request.acceptsCharsets(charsets)
...
洋蔥圈
使用層面
- koa 洋蔥圈執行機制圖解
- 洋蔥圈簡易實現版
執行方式: tsc onionRings.ts --lib 'es2015' --sourceMap && node onionRings.js
main code
public use(middleware: Function) {
this.middList.push(middleware);
}
// 執行器
private async deal(i: number = 0) {
debugger;
if (i >= this.middList.length) {
return false;
}
await this.middList[i](this, this.deal.bind(this, i + 1));
}
實現思路
- use 方法註冊 middleware。
- deal 模擬一個執行器: 大致思路就是將下一個 middleware 作爲上一個 middleware 的 next 去 await,用以保證正確的執行順序和中斷。
問題
如果習慣了回調的思路, 你會不會有這種疑惑: 洋蔥圈機制於在 一箇中間件中調用另一箇中間件,被調中間件執行成功,回到當前中間件繼續往後執行,這樣不斷調用,中間件很多的話, 會不會形成一個很深的函數調用棧? 從而影響性能, 同時形成「xx 地獄」? -- ps(此問題源於分享時原同事 小龍 的提問。)
實際上這是個很好的問題,對函數執行機制比較瞭解纔會產生的疑問。排除異步代碼處理,我們很容易用同步方式模擬出這種調用層級。參考: 同步方式。 這種模式存在明顯的調用棧問題。
我可以負責任的回答: 不會的,下一個問題。 😂 😂
不會的原因在 generator 中詳細介紹,一兩句說不清楚。實際上我認爲這裏是有語法門檻的。在 generator 之前,用任何方式處理這個問題,都顯得怪異,而且難以解調用決層級帶來的性能, 調試等帶來問題。
詳細說明參考: generator 真.協程
源碼
KOA 源碼特別精簡, 不像 Express 封裝的功能那麼多, git 源碼: 【https://github.com/koajs/koa】
工程
koa2 的源碼工程結構非常簡潔,一目瞭然, 沒有花裏胡哨的東西。
主文件
├── History.md
├── ....
├── Readme.md
├── benchmarks
├── docs // doc
│ ├── api ......
├── lib // 源碼
│ ├── application.js // 入口文件,封裝了context,request,response,核心的中間件處理流程。
│ ├── context.js // context.js 處理應用上下文,裏面直接封裝部分request.js和response.js的方法
│ ├── request.js // request.js 處理http請求
│ └── response.js // response.js 處理http響應
├── package.json
└── test // 測試模塊
├── application
....
package.json
- jest 做測試
-
node 版本
{ "engines": { "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" } }
- 主入口:
"main": "lib/application.js"
koa 核心模塊
- 封裝的 http server(node)
- 核心對象 context, request、response
- 中間件機制和剝洋蔥模型的實現
- 錯誤捕獲和錯誤處理
源碼
-
application.js
application.js 是 koa 的入口,繼承了events , 所以框架有事件監聽和事件觸發的能力。application 還暴露了一些常用的api,比如toJSON、listen、use等等。 - context.js
- request.js
- response.js
特殊處理
委託
- 摘自 context.js:context.js
const proto = module.exports = {
// ...
};
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.access('accept')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
koa 爲了方便串聯中間件,提供了一個 context 對象,並且把核心的 response, request 對象掛載在上面, 但是這樣往往就造成使用上寫法冗餘, eg: ctx.response.body
, 而且某些對象還是經常使用的,這很不方便,所以產生了 delegates 庫,用於委託操作, 委託之後,就可以在 ctx 上直接使用部分委託屬性: ctx.body
。源碼分析如下
middleware 機制
koa 中 use 用來註冊中間件,實際上是將多箇中間件放入一個緩存隊列中 this.middleware.push(fn);
,然後通過koa-compose這個插件進行遞歸組合。
因此嚴格來講 middleware 的執行結構的組織並不在 koa 源碼中完成,而是在依賴庫 koa-compose
中。 koa 中使用: const fn = compose(this.middleware);
完成中間件的組合。
koa-compose 核心邏輯如下, 主要思路大致是: 通過包裝 middleware List 返回一個 組裝好的執行器。
組裝思路是:將下一個 middleware 進行包裝【執行器 + promise 化】作爲上一個 middleware 的 next【dispatch.bind(null, i + 1)】。同時給中間件提供 context 對象。
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
// 函數洋蔥的最後補上一個Promise.resolve();
if (!fn) return Promise.resolve()
try {
// middleware 是 async 函數, 返回 promise 。Promise.resolve 確保中間件執行完成
// 提供 ctx, next fn: dispatch.bind(null, i + 1)
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
koa-compose
koa-compose 是一個非常精簡的庫,不做單獨分析了, 他提供了一種主調型的遞歸: fn(context, dispatch.bind(null, i + 1))
, 這種方式可以認爲是'懶遞歸', 將遞歸的執行交給主調者控制,這樣能夠在更合適的時機執行後續處理, 但如果某個中間件不調用 next,那麼其後的中間件就不被執行了。這和 js 協程【generator】有機制上的類似,都是使用者來控制 next 的執行時機, 可類比學習。
易用性處理
koa 非常易用, 原因是 koa 在源碼層面做了大量的 委託 和針對複雜對象的封裝,如 request, response 的 get/set. 用以提高工具的可用度,易用度。實際上我認爲這一點是現代框架非常重要的東西,脫離用戶的庫都不是好庫。koa 是好庫。
- delegates 上面說過, 參考: delegates。
- get/set
request, response 兩個文件千行代碼, 80% 左右的都是 get、set,參考:
另一方面,表現在 application.js 的 createContext
方法中,通過掛載引用和委託配合 get set 的實踐配合提升易用度,單獨不太好講,分析註釋在源碼中。
異常捕獲
- 中間件異常捕獲, koa1 中間件基於 generator + co, koa2 中間件基於 async/await, async 函數返回 promise, 所以只要在組合中間件後 catch 即可捕獲中間件異常
fnMiddleware(ctx).then(handleResponse).catch(onerror);
- 框架層發生錯誤的捕獲機制, 這個通過繼承 event 模塊很容易實現監聽。
this.on('error', this.onerror);
註冊的 error 事件, 在 context.onerror 中被 emit
this.app.emit('error', err, this);
- http 異常處理 : Execute a callback when a HTTP request closes, finishes, or errors.
onFinished(res, onerror); // application.handleRequest
中間件交互
初用中間件可能會有一個疑問: 中間件如何通信?
事實上這是個設計取捨邏輯, 中間件之間的數據交互並不是麻煩事, 特別是在 ECMAScript 推出 async await 之後,但問題是這樣做的意義不大,原因是所有的中間件是可任意插拔組合的,這種不確定性,導致了中間件之間的數據交互就變得不穩定,最起碼的數據格式就沒辦法固定,就更別談處理了。靈活的插件機制導致中間件之間的交互難有統一層面的實現。
另一方面從中間件的定位來看,其之間也沒必要交互,中間件不能脫離 http 的請求響應而獨立存在,他是服務於整個過程的,也因此所有的中間件第一個參數就是 ctx, 這個對象掛載了 request 和 response, 以及 koa 提供的封裝和工具操作。
核心點
中斷
這是洋蔥圈非常核心的支撐點, 我們稍微留意就能發現 koa 中間件執行機制於普通 js 的執行順序很不一致, 我們看如下代碼:
app.use(async (cxt, next) => {
Log(1);
await next();
Log(2);
});
app.use(async (cxt, next) => {
Log(3);
await next();
Log(4);
});
上述代碼執行順序也就是洋蔥圈: Log(1) -> await next (Log(3)) -> await next -> Log(4) -> Log(2).
爲了保證代碼按照洋蔥模型的執行順序執行,程序需要在調用 next 的時候讓代碼等待,我稱之爲中斷。
實際上以前想要實現這種執行順序,只能依賴 cb, promise.then 來模擬,而且即便實現了,在寫法上也顯得臃腫和彆扭,要麼是寫出很胖的函數,要麼是寫出很長的函數。而且沒法處理調用棧的問題。
async/await 可以比較優雅的實現這種具有同步執行特徵的前端代碼來處理異步,代碼執行到 await 這裏,等待 await 表達式的執行,執行完成之後,接着往後執行。
實際上這很類似於 generator 的 yield,特性。async 也就是 generator + 執行器的一個語法糖, 參考:
async ? no , it's generator
koa.use 得確直接使用 async 函數處理中間件及其中可能存在的異步, 而 async/await 實現上是基於 generator 。async 在使用上可講的點通常在他的 task 放在哪,以及執行時機 和 timeout ,promise 的執行順序等。真正的中斷特性得益於 generator。
一位不願透漏姓名的同事問了我一個問題,怎麼證明 async 是 generator + 執行器 的語法糖?這是不得不討論一個問題。相關的討論參考: Async / Await > #generator 部分探討
生態
koa 中間件並沒有一個統一的 market 之類的地方,說實話找起來不是那麼方便。如果你想找中間件的話,可以在 npm 上用 koa-
做關鍵字檢索: https://www.npmjs.com/search?...
源碼使用的中間件
-
koa-compose
上面已有分析 -
function isJSON(body) { if (!body) return false; if ('string' == typeof body) return false; if ('function' == typeof body.pipe) return false; if (Buffer.isBuffer(body)) return false; return true; }
-
koa-convert
用於兼容處理 generator 中間件,基本可以認爲是 co + generator 中間件【也依賴 koa-compose 進行組織】
other koa
社區常用中間件合集: some middleware