Koa2 源碼賞析

前言

隨著 Node 新版本開始支持 async/await 非同步控制寫法,Koa 也相繼發布了它的 2.0 版本。用 async/await 寫法的 Koa 來開發項目,Node 開發者再也沒有任何理由不「擁抱變化」——從 Express 轉到 Koa 上來。實際上,對於普通 Node 開發者——Express 框架的用戶——而言,從 Express 轉到 Koa 沒有任何技術壁壘,當然前提是你至少得知道 ES2017 中 async/await 是個什麼東西。

有了 Express 的積累,以 TJ 為首的 Express 開發團隊對於 Koa 的設計也更加的得心應手、遊刃有餘。從源代碼來看,Koa 看起來甚至比 Express 更加簡潔和靈活,然而在功能上卻絲毫沒有讓步,甚至更勝一籌。一方面,這得益於 JavaScript 越來越方便好用的各種「語法糖」;另一方面,也在於 Koa 本身簡潔強大的設計:它不再綁定任何特定的中間件,也去掉了其他任何多餘的設計(連路由系統都抽象成了第三方中間件),而只是簡單的提供了一個優雅管理各種中間件的約束系統,用戶的所有掛載都是中間件。

TJ 大神的代碼一向簡潔強大,Koa 的源碼也是如此。如果你去 GitHub 上查看 Koa 的源碼,你同樣會被其簡潔所震撼,核心代碼不過 4 個文件,平均每個文件代碼行數也就四百來行,看似簡簡單單,卻天才般的把 Express 線性的中間件控制流轉變為「洋蔥體」結構,從而解鎖了更多的姿勢和玩法。本文就直接深入 Koa 的源碼(v2.2.0),來欣賞 Koa 的曼妙身姿。

代碼大體結構

lib 目錄下總共就四個文件:application.js、context.js、request.js 和 response.js。入口文件是 application.js,導出的是一個繼承了 Node 內建模塊 Events 的 Class,構造函數中進行了必要的參數初始化,並且把 context、request 和 response 屬性指向了原型鏈指向其他三個文件導出對象的實例。

然後是類方法,主要的幾個 public 的方法如下:

  • listen:一個簡單的對 http.createServer(this.callback()).listen(...) 的封裝。
  • callback:在 listen 中有調用,返回一個用於 http.createServer 的回調函數 handleRequest,在這個函數中創建了主角 ctx,並做了一些原型鏈繼承和 aliases。更重要的是,調用的 koa-compose 返回了一個 fn 函數,負責了整個中間件「洋蔥體」流程的實現和控制。在所有中間件執行完之後,做了一些返回之前的瑣碎諸如設置必要的返回頭等的工作。
  • use:把中間件參數放入 this.middleware 數組,並返回 this 以便鏈式調用。

結合 Koa 文檔和 application.js,基本就可以對整個框架的處理流程有個整體的把握了。其中最主要的部分還是 callback 函數中的內容,看完之後對整個基於 Node HTTP 模塊封裝的中間件處理的流轉過程都清楚了。

Context

這裡是對 this.context 的原型對象的實現。

沒有太多值得一提的東西,基本是對上下文對象 ctx 提供幾個必需的原型介面以及一個預設的錯誤處理函數 onerror。有意思的是,利用 delegates 包,把對 app 的一些屬性的訪問直接對應的代理到 response 和 request 上,這也就是文檔上所說的 Request aliases 和 Response aliases 的具體原因。

Request & Response

這倆文件是對 this.request 和 this.response 的原型對象的實現。

this.request 和 this.response 中都有大量屬性的 getter 和 setter 方法,這些可用的屬性在 Koa 文檔中都已經列出,代碼在這裡對它們的讀寫操作進行了實現。這些屬性基本是對 Node HTTP 包中 req 和 res 屬性的封裝。

「洋蔥體」帶來了什麼

你固然還是可以像在 Express 中一樣把 next(); 都放在每個中間件的末尾來線性的傳遞控制權,但「回形針」式的控制流帶來了更多的可能。一個最典型的的例子就是 response-time 中間件的實現。

在 GitHub 上用 response-time 關鍵字搜索,前兩個 Repo 就分別是 Express 和 Koa 中對這個中間件的實現。

Express 中的實現其實是 hack 了被 Express 用到的 Node.js 內部 HTTP 模塊的 res.writeHead 方法(實際實現細節在 response-time 中間件調用的 on-headers 包中),使得在這一層注入了一小段代碼用於在數據返回前計算時間差並寫入 Response Headers。這樣雖然可以實現,但顯然不夠好。它 hack 了框架底層的一個內部方法,雖然也巧妙,但代碼本身並不是在給它天生就安排好的合適的地方來實現的,可以視為晦澀的「奇技淫巧」,而且與 Express 的內部實現強耦合,指不定哪天 Express 改了 res.writeHead 調用時機,這個中間件的返回值可能就有所不同了(當然按實際情況來說 Express 此處應該也不會改了,而且 Express 的 response-time 最初也是 TJ 寫的)。

Koa 的 response-time 實現自不必多說,官方文檔上就有,來欣賞它的簡潔優雅:

app.use(async function (ctx, next) {n const start = new Date();n await next();n const ms = new Date() - start;n ctx.set(X-Response-Time, `${ms}ms`);n});n

總結來說,Koa 的「洋蔥體」結構使得每個中間件能夠在同一次請求的前後對稱的部分提供相同的上下文環境,這樣就讓實現像 response-time 這樣的中間件變得相當簡單。

推薦閱讀

  • 編寫可維護代碼之「中間件模式」

---- END ----

如果你想第一時間查看我最新的文章,歡迎 RSS 訂閱我的個人博客:Maples7s Blog。知乎專欄將延期數天到數月不等不完全同步博客中的文章。

微信公眾號:Chapters_Of_Maples7,只更新自己隨手寫的想到的隻言片語或圖片。

本文內容可能已經不是最新,查看原文:Koa2 源碼賞析


推薦閱讀:

Node.js 性能調優之內存篇(一)——gcore+llnode
編寫 Node.js Rest API 的 10 個最佳實踐
express4.x Request對象獲得參數方法小談
4.2 目錄結構-博客後端Api-NodeJs+Express+Mysql實戰

TAG:Nodejs | Express框架 | koa |