koa2 一網打盡(基本使用,洋蔥圈,中間件機制和模擬,源碼分析(工程,核心模塊,特殊處理),核心點,生態)

原創禁止私自轉載


koa2

koa homepage

優秀的下一代 web 開發框架。
Koa 應用程序不是 HTTP 服務器的1對1展現。 可以將一個或多個 Koa 應用程序安裝在一起以形成具有單個HTTP服務器的更大應用程序。

基礎使用

快速搭建簡易 koa server 服務

koa 搭建一個服務還是很簡單的, 主要代碼如下, 完整代碼如下. 切到主目錄下,

  1. 安裝依賴: yarn
  2. 執行入口: 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)

...

更多參考

洋蔥圈

使用層面

  1. koa 洋蔥圈執行機制圖解

clipboard.png

  1. 洋蔥圈簡易實現版
執行方式: 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));
}

實現思路

  1. use 方法註冊 middleware。
  2. 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
  1. jest 做測試
  2. node 版本

    {
        "engines": {
            "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4"
        }
    }
  3. 主入口: "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

特殊處理

委託

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-compose

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.jscreateContext 方法中,通過掛載引用和委託配合 get set 的實踐配合提升易用度,單獨不太好講,分析註釋在源碼中。

異常捕獲

  1. 中間件異常捕獲, koa1 中間件基於 generator + co, koa2 中間件基於 async/await, async 函數返回 promise, 所以只要在組合中間件後 catch 即可捕獲中間件異常
    fnMiddleware(ctx).then(handleResponse).catch(onerror);
  2. 框架層發生錯誤的捕獲機制, 這個通過繼承 event 模塊很容易實現監聽。
    this.on('error', this.onerror);

    註冊的 error 事件, 在 context.onerror 中被 emit this.app.emit('error', err, this);

  3. 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?...

官方 middleware

源碼使用的中間件

  • koa-compose
    上面已有分析
  • koa-is-json

    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

參考 & 鳴謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章