原創禁止私自轉載javascript
koa homepagecss
優秀的下一代 web 開發框架。
Koa 應用程序不是 HTTP 服務器的1對1展示。 能夠將一個或多個 Koa 應用程序安裝在一塊兒以造成具備單個HTTP服務器的更大應用程序。
koa 搭建一個服務仍是很簡單的, 主要代碼以下, 完整代碼以下. 切到主目錄下,html
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...
參考官網提供的基本 api ,不在贅述: https://koa.bootcss.com/前端
部分 api 實現,參考: 源碼分析java
context
Koa Context 將 node 的 request 和 response 對象封裝到單個對象中,爲編寫 Web 應用程序和 API 提供了許多有用的方法。 這些操做在 HTTP 服務器開發中頻繁使用,它們被添加到此級別而不是更高級別的框架,這將強制中間件從新實現此通用功能。__每一個__ 請求都將建立一個 Context,並在中間件中做爲接收器引用,或者 ctx 標識符。node
...git
更多參考github
執行方式:
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)); }
若是習慣了回調的思路, 你會不會有這種疑惑: 洋蔥圈機制於在 一箇中間件中調用另外一箇中間件,被調中間件執行成功,回到當前中間件繼續日後執行,這樣不斷調用,中間件不少的話, 會不會造成一個很深的函數調用棧? 從而影響性能, 同時造成「xx 地獄」? -- ps(此問題源於分享時原同事 小龍 的提問。)
實際上這是個很好的問題,對函數執行機制比較瞭解纔會產生的疑問。排除異步代碼處理,咱們很容易用同步方式模擬出這種調用層級。參考: 同步方式。 這種模式存在明顯的調用棧問題。web
我能夠負責任的回答: 不會的,下一個問題。 😂 😂typescript
不會的緣由在 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
node 版本
{ "engines": { "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" } }
"main": "lib/application.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
。源碼分析以下
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 是好庫。
request, response 兩個文件千行代碼, 80% 左右的都是 get、set,參考:
另外一方面,表如今 application.js 的 createContext
方法中,經過掛載引用和委託配合 get set 的實踐配合提高易用度,單獨不太好講,分析註釋在源碼中。
fnMiddleware(ctx).then(handleResponse).catch(onerror);
this.on('error', this.onerror);
註冊的 error 事件, 在 context.onerror 中被 emit this.app.emit('error', err, this);
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 + 執行器的一個語法糖, 參考:
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?...
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; }
社區經常使用中間件合集: some middleware