10分鐘理解 Node.js koa 原始碼架構設計

智聯大前端發表於2019-07-22

clipboard.png

koa 釋出已經快 6 年的時間,作為繼 express 之後 node 服務框架最大的黑馬,有很多的設計思想值得我們學習,本文從簡到繁逐步介紹 koa,同時適合新老手閱讀。

介紹

這裡引用中文官方網站的原文

Koa 是一個新的 web 框架,由 Express 幕後的原班人馬打造, 致力於成為 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。 通過利用 async 函式,Koa 幫你丟棄回撥函式,並有力地增強錯誤處理。 Koa 並沒有捆綁任何中介軟體, 而是提供了一套優雅的方法,幫助您快速而愉快地編寫服務端應用程式。

既然是 web 框架大家一定不陌生,通過啟動一個 node http server,監聽一個埠,進而我們就可以通過類似 localhost:3000 在本地訪問我們的服務了,這個服務可以是 web 網站,可以是 restful 介面,也可以是靜態檔案服務等等。

Hello Word

任何語言、框架都存在 Hello Word 示例,來表達其最簡單的入門 Demo,程式碼如下

clipboard.png

此時訪問瀏覽器 localhost:3000,我們會看到列印出了 Hello Word,此時一個基於 koa 的服務就啟動完成了。

上下文

理解 koa 第一步,搞清楚上下文的作用

例如:微信群裡面有人說外面下雪了,你跑到窗邊看到的卻是晴空萬里,這時你才意識到同樣是 10 月份,他在寒冷的北方,你在酷暑的南方

類似的,一次請求會包含使用者的登入狀態,或者一些Token之類的資訊,這些資訊就是上下文的一部分,用於確定一次的請求環境

Koa 的 Context 把 node 的 request, response 物件封裝進一個單獨物件, 並提供許多開發 web 應用和 APIs 有用的方法. 那些在 HTTP server 開發中使用非常頻繁操作, 直接在 Koa 裡實現, 而不是放在更高層次的框架, 這樣中介軟體就不需要重複實現這些通用的功能。

中介軟體

先來看一個官方的例子:

clipboard.png

簡單解釋下,程式碼起始初始化一個 koa 例項,下面分別通過 use 方法載入了三個中介軟體方法,執行順序:

  1. 進入第一個中介軟體
  2. next() 跳到下一個中介軟體
  3. new Data() 記錄當前時間
  4. next() 跳到下一個中介軟體
  5. ctx.body 賦值
  6. 回到上一個中介軟體再次記錄當前時間並計算時間差存到 http header
  7. 回到上一個中介軟體將 header 中的 X-Response-time 列印出來

這裡的執行順序延伸出了十分經典的洋蔥模型

clipboard.png

在一次請求的過程中會往返經過同一中介軟體兩次,允許我們處理不同請求階段的邏輯

原始碼解析

上面分別介紹了 koa 裡面兩個最重要的概念,下面我們分析下 koa 內部是如何運作的,所謂的洋蔥模型是如何建立的

koa 原始碼的 lib 目錄十分簡單

lib
  |- application.js
  |- context.js
  |- request.js
  |- response.js

Application 類初始化

入口檔案是 application.js,我們先從這裡入手

clipboard.png

Application 是一個 class,這個類繼承了 node 的 Events 這裡不詳細展開,在 constructor 中初始化了以下內容:

  • proxy 代理預設不開啟
  • middleware 中介軟體是個空陣列,這裡重點注意下
  • env 根據環境變數 NODE_ENV 來判斷
  • context、request、response 分別通過 Object.create 方法將 lib 目錄下對應的檔案匯入到 this 當前上下文,且不汙染引入物件

use 方法

按照正常的編碼順序,在初始化完 koa 例項後(即 const app = new Koa()),我們需要呼叫 app.use() 去掛載我們的中介軟體,那麼我們看下 use 方法做了什麼

clipboard.png

判斷中介軟體為 function,判斷中介軟體是否為 generator function 型別,只是簡單的將中介軟體函式 push 到了 middleware 陣列中。

此時心中有沒有大寫的 WHAT?

其實就是這麼直白,沒什麼複雜邏輯,後面也許大家都猜到了,迴圈呼叫 middleware 中的方法去執行,此處尚未表明洋蔥模型是怎麼來的,我們先不展開,繼續按程式碼順序執行。

listen 方法

按照正常的編碼順序,在 use 完我們的中介軟體之後就是 app.listen(3000)

一起看下這個 listen 幹了什麼

clipboard.png

這裡的 http.createServer 就是 node 原生啟動 http 服務的方法,這裡稍微擴充套件下基礎知識,此方法接受兩個引數

  • options[IncomingMessage, ServerResponse] 這裡從 node 版本 v9.6.0, v8.12.0 後才支援,這裡不贅述
  • requestListener 此引數為 function 型別,每次請求會傳入 req, res 兩個引數

不難理解這裡的 this.callback() 方法一定是返回了一個函式,並且接收兩個引數 (req, res),下面看下原始碼

clipboard.png

這個 callback 中的資訊量有點大,程式碼本身並不難理解,註釋也有說明,從這裡展開從上到下分別解釋

compose 方法

這裡的 compose 方法主要負責生成洋蔥模型,通過 koa-compose 包實現,原始碼如下

clipboard.png

從註釋看得出大致邏輯,這裡的巧妙之處在於 fn(context, dispatch.bind(null, i + 1))

這個 dispatch.bind(null, i + 1) 就是我們通常寫中介軟體的第二個引數 next

我們執行這個 next() 方法實際上得到的是下一個中介軟體的執行。

也就不難理解為什麼我們 await next() 的時候等待的是後面所有中介軟體串聯執行後了,回頭再看下上文中介軟體部分的執行順序就豁然開朗了。

createContext 方法

callback 中的展開解釋,看下 const ctx = this.createContext(req, res) 做了什麼

clipboard.png

這裡主要是將 req, res 及 this.request, this.response 都掛載到了 context 上,並通過賦值理清了迴圈引用層級關係,為使用者提供方便。

handleRequest 方法

還是 callback 中的展開解釋,看下 this.handleRequest(ctx, fn) 這部分做了什麼

clipboard.png

分別拿到 ctx 和 compose 生成的洋蔥模型,開始逐一消費中介軟體。

context.js 檔案

上面理清了整體框架,下面看下 context.js 內部的細節,在檔案結尾有兩大段的代理

clipboard.png

clipboard.png

這裡可以看到所有的 req 及 res 的方法集合,那麼哪些方法可讀,哪些可寫,哪些既可讀又可寫,哪些方法不允許修改

這就是 delegates 這個庫做的事情。

delegates 內部利用了,__defineGetter____defineSetter__ 方法控制讀寫,當然我們可以從中學習思想,也不能盲從

這兩個 api 去 MDN 上搜尋會給出相同的警告資訊

This feature is deprecated in favor of defining setters using the object initializer syntax or the Object.defineProperty() API.

其實還是建議我們使用 vue 的代理方式 Object.defineProperty(),不過這個庫有四年沒更新了依然穩定執行著,還是深受 koa 開發者認可的。

其它

request.jsresponse.js 檔案沒什麼可以講,就是具體的工具方法實現,方便開發人員呼叫,感興趣可以自行閱讀原始碼。

應用

智聯前端架構整體的 node 服務都基於 koa 實現,包括我們的 vue 服務端渲染和 node restful api 等等。

我們選擇 koa 的原因是其本身輕巧,可擴充套件性良好,支援 async、await 的非同步,徹底擺脫了回撥地獄。

市面上也有成熟基於 koa2 的企業級解決方案,如 eggjs 和 thinkjs。

總結

揭開 koa 的神祕面紗,讓開發者關注業務邏輯同時也關注下框架本身,有利於問題排查和編寫擴充套件,與此同時可以學習 express、hapi 等同型別框架的思想,結合現有企業級解決方案,選一款適合你的框架,總之框架不論好壞,只論場景。

相關文章