koa源碼閱讀[0]

来源:https://www.cnblogs.com/jiasm/archive/2018/07/22/9351234.html
-Advertisement-
Play Games

koa源碼閱讀[0] Node.js也是寫了兩三年的時間了,剛開始學習Node的時候,hello world就是創建一個HttpServer,後來在工作中也是經歷過Express、Koa1.x、Koa2.x以及最近還在研究的結合著TypeScript的routing-controllers(驅動依然 ...


koa源碼閱讀[0]

Node.js也是寫了兩三年的時間了,剛開始學習Node的時候,hello world就是創建一個HttpServer,後來在工作中也是經歷過ExpressKoa1.xKoa2.x以及最近還在研究的結合著TypeScriptrouting-controllers(驅動依然是ExpressKoa)。
用的比較多的還是Koa版本,也是對它的洋蔥模型比較感興趣,所以最近抽出時間來閱讀其源碼,正好近期可能會對一個Express項目進行重構,將其重構為koa2.x版本的,所以,閱讀其源碼對於重構也是一種有效的幫助。

Koa是怎麼來的

首先需要確定,Koa是什麼。
任何一個框架的出現都是為瞭解決問題,而Koa則是為了更方便的構建http服務而出現的。
可以簡單的理解為一個HTTP服務的中間件框架。

使用http模塊創建http服務

相信大家在學習Node時,應該都寫過類似這樣的代碼:

const http = require('http')

const serverHandler = (request, response) => {
  response.end('Hello World') // 返回數據
}

http
  .createServer(serverHandler)
  .listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))

 

一個最簡單的示例,腳本運行後訪問http://127.0.0.1:8888即可看到一個Hello World的字元串。
但是這僅僅是一個簡單的示例,因為我們不管訪問什麼地址(甚至修改請求的Method),都總是會獲取到這個字元串:

> curl http://127.0.0.1:8888
> curl http://127.0.0.1:8888/sub
> curl -X POST http://127.0.0.1:8888

 

所以我們可能會在回調中添加邏輯,根據路徑、Method來返回給用戶對應的數據:

const serverHandler = (request, response) => {
  // default
  let responseData = '404'

  if (request.url === '/') {
    if (request.method === 'GET') {
      responseData = 'Hello World'
    } else if (request.method === 'POST') {
      responseData = 'Hello World With POST'
    }
  } else if (request.url === '/sub') {
    responseData = 'sub page'
  }

  response.end(responseData) // 返回數據
}

 

類似Express的實現

但是這樣的寫法還會帶來另一個問題,如果是一個很大的項目,存在N多的介面。
如果都寫在這一個handler裡邊去,未免太過難以維護。
示例只是簡單的針對一個變數進行賦值,但是真實的項目不會有這麼簡單的邏輯存在的。
所以,我們針對handler進行一次抽象,讓我們能夠方便的管理路徑:

class App {
  constructor() {
    this.handlers = {}

    this.get = this.route.bind(this, 'GET')
    this.post = this.route.bind(this, 'POST')
  }

  route(method, path, handler) {
    let pathInfo = (this.handlers[path] = this.handlers[path] || {})

    // register handler
    pathInfo[method] = handler
  }

  callback() {
    return (request, response) => {
      let { url: path, method } = request

      this.handlers[path] && this.handlers[path][method]
        ? this.handlers[path][method](request, response)
        : response.end('404')
    }
  }
}

 

然後通過實例化一個Router對象進行註冊對應的路徑,最後啟動服務:

const app = new App()

app.get('/', function (request, response) {
  response.end('Hello World')
})

app.post('/', function (request, response) {
  response.end('Hello World With POST')
})

app.get('/sub', function (request, response) {
  response.end('sub page')
})

http
  .createServer(app.callback())
  .listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))

 

Express中的中間件

這樣,就實現了一個代碼比較整潔的HttpServer,但功能上依舊是很簡陋的。
如果我們現在有一個需求,要在部分請求的前邊添加一些參數的生成,比如一個請求的唯一ID。
將代碼重覆編寫在我們的handler中肯定是不可取的。
所以我們要針對route的處理進行優化,使其支持傳入多個handler

route(method, path, ...handler) {
  let pathInfo = (this.handlers[path] = this.handlers[path] || {})

  // register handler
  pathInfo[method] = handler
}

callback() {
  return (request, response) => {
    let { url: path, method } = request

    let handlers = this.handlers[path] && this.handlers[path][method]

    if (handlers) {
      let context = {}
      function next(handlers, index = 0) {
        handlers[index] &&
          handlers[index].call(context, request, response, () =>
            next(handlers, index + 1)
          )
      }

      next(handlers)
    } else {
      response.end('404')
    }
  }
}

 

然後針對上邊的路徑監聽添加其他的handler:

function generatorId(request, response, next) {
  this.id = 123
  next()
}

app.get('/', generatorId, function(request, response) {
  response.end(`Hello World ${this.id}`)
})

 

這樣在訪問介面時,就可以看到Hello World 123的字樣了。
這個就可以簡單的認為是在Express中實現的 中間件
中間件是ExpressKoa的核心所在,一切依賴都通過中間件來進行載入。

更靈活的中間件方案-洋蔥模型

上述方案的確可以讓人很方便的使用一些中間件,在流程式控制制中調用next()來進入下一個環節,整個流程變得很清晰。
但是依然存在一些局限性。
例如如果我們需要進行一些介面的耗時統計,在Express有這麼幾種可以實現的方案:

function beforeRequest(request, response, next) {
  this.requestTime = new Date().valueOf()

  next()
}

// 方案1. 修改原handler處理邏輯,進行耗時的統計,然後end發送數據
app.get('/a', beforeRequest, function(request, response) {
  // 請求耗時的統計
  console.log(
    `${request.url} duration: ${new Date().valueOf() - this.requestTime}`
  )

  response.end('XXX')
})

// 方案2. 將輸出數據的邏輯挪到一個後置的中間件中
function afterRequest(request, response, next) {
  // 請求耗時的統計
  console.log(
    `${request.url} duration: ${new Date().valueOf() - this.requestTime}`
  )

  response.end(this.body)
}

app.get(
  '/b',
  beforeRequest,
  function(request, response, next) {
    this.body = 'XXX'

    next() // 記得調用,不然中間件在這裡就終止了
  },
  afterRequest
)

 

無論是哪一種方案,對於原有代碼都是一種破壞性的修改,這是不可取的。
因為Express採用了response.end()的方式來向介面請求方返回數據,調用後即會終止後續代碼的執行。
而且因為當時沒有一個很好的方案去等待某個中間件中的非同步函數的執行。

function a(_, _, next) {
  console.log('before a')
  let results = next()
  console.log('after a')
}

function b(_, _, next) {
  console.log('before b')
  setTimeout(_ => {
    this.body = 123456
    next()
  }, 1000)
}

function c(_, response) {
  console.log('before c')
  response.end(this.body)
}

app.get('/', a, b, c)

 

就像上述的示例,實際上log的輸出順序為:

before a
before b
after a
before c

 

這顯然不符合我們的預期,所以在Express中獲取next()的返回值是沒有意義的。

所以就有了Koa帶來的洋蔥模型,在Koa1.x出現的時間,正好趕上了Node支持了新的語法,Generator函數及Promise的定義。
所以才有了co這樣令人驚嘆的庫,而當我們的中間件使用了Promise以後,前一個中間件就可以很輕易的在後續代碼執行完畢後再處理自己的事情。
但是,Generator本身的作用並不是用來幫助我們更輕鬆的使用Promise來做非同步流程的控制。
所以,隨著Node7.6版本的發出,支持了asyncawait語法,社區也推出了Koa2.x,使用async語法替換之前的co+Generator

Koa也將co從依賴中移除(2.x版本使用koa-convertGenerator函數轉換為promise,在3.x版本中將直接不支持Generator
ref: remove generator supports

由於在功能、使用上Koa的兩個版本之間並沒有什麼區別,最多就是一些語法的調整,所以會直接跳過一些Koa1.x相關的東西,直奔主題。

Koa中,可以使用如下的方式來定義中間件並使用:

async function log(ctx, next) {
  let requestTime = new Date().valueOf()
  await next()

  console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`)
}

router.get('/', log, ctx => {
  // do something...
})

 

因為一些語法糖的存在,遮蓋了代碼實際運行的過程,所以,我們使用Promise來還原一下上述代碼:

function log() {
  return new Promise((resolve, reject) => {
    let requestTime = new Date().valueOf()
    next().then(_ => {
      console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`)
    }).then(resolve)
  })
}

 

大致代碼是這樣的,也就是說,調用next會給我們返回一個Promise對象,而Promise何時會resolve就是Koa內部做的處理。
可以簡單的實現一下(關於上邊實現的App類,僅僅需要修改callback即可):

callback() {
  return (request, response) => {
    let { url: path, method } = request

    let handlers = this.handlers[path] && this.handlers[path][method]

    if (handlers) {
      let context = { url: request.url }
      function next(handlers, index = 0) {
        return new Promise((resolve, reject) => {
          if (!handlers[index]) return resolve()

          handlers[index](context, () => next(handlers, index + 1)).then(
            resolve,
            reject
          )
        })
      }

      next(handlers).then(_ => {
        // 結束請求
        response.end(context.body || '404')
      })
    } else {
      response.end('404')
    }
  }
}

 

每次調用中間件時就監聽then,並將當前Promiseresolvereject處理傳入Promise的回調中。
也就是說,只有當第二個中間件的resolve被調用時,第一個中間件的then回調才會執行。
這樣就實現了一個洋蔥模型。

就像我們的log中間件執行的流程:

  1. 獲取當前的時間戳requestTime
  2. 調用next()執行後續的中間件,並監聽其回調
  3. 第二個中間件裡邊可能會調用第三個、第四個、第五個,但這都不是log所關心的,log只關心第二個中間件何時resolve,而第二個中間件的resolve則依賴他後邊的中間件的resolve
  4. 等到第二個中間件resolve,這就意味著後續沒有其他的中間件在執行了(全都resolve了),此時log才會繼續後續代碼的執行

所以就像洋蔥一樣一層一層的包裹,最外層是最大的,是最先執行的,也是最後執行的。(在一個完整的請求中,next之前最先執行,next之後最後執行)。

小記

最近抽時間將Koa相關的源碼翻看一波,看得挺激動的,想要將它們記錄下來。
應該會拆分為幾段來,不一篇全寫了,上次寫了個裝飾器的,太長,看得自己都困了。
先占幾個坑:

  • 核心模塊 koa與koa-compose
  • 熱門中間件 koa-router與koa-views
  • 雜七雜八的輪子 koa-bodyparser/multer/better-body/static

示例代碼倉庫地址
源碼閱讀倉庫地址


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Hive官方文檔:Home-UserDocumentation Hive DDL官方文檔:LanguageManual DDL 參考文章:Hive 用戶指南 註意:各個語句的版本時間,有的是在 hive-1.2.1 之後才有的,這些語句我們在hive-1.2.1中是不能使用的。 註意:本文寫的都是常 ...
  • 28年前有人發明www microsoft技術開發人員lot 看論文可以看中國知網 微軟亞洲研究院 WWDC蘋果開發者大會上,蘋果都會發佈一些新的公司發展出的新的產品的新技術。iOS開發,用到的語言有:objective-c swift 還有cocoa touch框架 storyboarding(故 ...
  • 相信做前端的都做過頁面錨點定位的功能,通過`` 去設置頁面內錨點定位跳轉。 本篇文章就使用 、`scrollview`來實現android錨點定位的功能。 效果圖: 實現思路 1、監聽 滑動到的位置, 切換到對應標簽 2、 各標簽點擊, 可滑動到對應區域 自定義scrollview 因為我們需要監聽 ...
  • 因為我們須用節點spritenode.copy()把其它Scene(CircleScene.sks)里的節點拷貝一份,並把拷貝的這一份節點加到當前的scene(GameScene.sks)里,還有一個重要的註意點,就是如果要在GameScene.sks取得 Overlay內的紅色節點的準確坐標,須用... ...
  • flash交互課件能生動表達教學內容,也深受廣大教育工作者的喜愛,但是目前flash課件只能在pc電腦平臺上進行展示,且目前蘋果公司已經不再支持flash各類產品,也就是後續蘋果ios pc系統也已經不能再使用flash產品,隨著移動網路的發展,越來越多的課件產品需要移殖到各種移動平臺(手機,pad ...
  • HTML代碼 CSS代碼 ...
  • 字元串 字元串就是零個或多個排在一起的字元,放在單引號或雙引號之中。 單引號字元串的內部,可以使用雙引號。雙引號字元串的內部,可以使用單引號。 多行與轉義 如果要在單引號字元串的內部,使用單引號(或者在雙引號字元串的內部,使用雙引號),就必須在內部的單引號(或者雙引號)前面加上反斜杠,用來轉義。 字 ...
  • Javascript設計模式-工廠模式 我理解工廠模式,就是把相關的多個類提供一個統一入口的一個模式,讓你從一個入口就可以獲得多個類,提高工作效率. 但是對於工廠模式也會有三種類型的實現方式,分別是:簡單工廠模式,工廠方法模式和抽象工廠模式.它們分別是在各自基礎上有一定的改進. 簡單工廠模式 也被叫 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...