一文搞懂 koa2 核心原理

koa 的基礎結構

首先,讓我們認識一下 koa 框架的定位——koa 是一個精簡的 node 框架:

koa 框架的核心目錄如下:

── lib
   ├── application.js
   ├── context.js
   ├── request.js
   └── response.js

// 每個文件的具體功能
── lib
   ├── new Koa()  || ctx.app
   ├── ctx
   ├── ctx.req  || ctx.request
   └── ctx.res  || ctx.response
複製代碼

undefined

koa 源碼基礎骨架

application.js application.js 是 koa 的主入口,也是核心部分,主要乾了以下幾件事情:

  1. 完成了 koa 實例初始化的工作,啓動服務器

  2. 實現了洋蔥模型的中間件機制

  3. 封裝了高內聚的 context 對象

  4. 實現了異步函數的統一錯誤處理機制

context.js context.js 主要乾了兩件事情:

  1. 完成了錯誤事件處理

  2. 代理了 response 對象和 request 對象的部分屬性和方法

request.js request 對象基於 node 原生 req 封裝了一系列便利屬性和方法,供處理請求時調用。所以當你訪問 ctx.request.xxx 的時候,實際上是在訪問 request 對象上的 setter 和 getter。

response.js response 對象基於 node 原生 res 封裝了一系列便利屬性和方法,供處理請求時調用。所以當你訪問 ctx.response.xxx 的時候,實際上是在訪問 response 對象上的 setter 和 getter。

4 個文件的代碼結構如下:

undefined

undefined

koa 工作流

Koa 整個流程可以分成三步:

  1. 初始化階段

new 初始化一個實例,包括創建中間件數組、創建 context/request/response 對象,再使用 use(fn) 添加中間件到 middleware 數組,最後使用 listen 合成中間件 fnMiddleware,按照洋蔥模型依次執行中間件,返回一個 callback 函數給 http.createServer,開啓服務器,等待 http 請求。結構圖如下圖所示:

undefined

  1. 請求階段

每次請求,createContext 生成一個新的 ctx,傳給 fnMiddleware,觸發中間件的整個流程。3. 響應階段 整個中間件完成後,調用 respond 方法,對請求做最後的處理,返回響應給客戶端。

koa 中間件機制與實現

koa 中間件機制是採用 koa-compose 實現的,compose 函數接收 middleware 數組作爲參數,middleware 中每個對象都是 async 函數,返回一個以 context 和 next 作爲入參的函數,我們跟源碼一樣,稱其爲 fnMiddleware 在外部調用 this.handleRequest 的最後一行,運行了中間件:fnMiddleware(ctx).then(handleResponse).catch(onerror);

以下是koa-compose庫中的核心函數:

我們不禁會問:中間件中的next到底是什麼呢?爲什麼執行next就進入到了下一個中間件了呢?中間件所構成的執行棧如下圖所示,其中next就是一個含有dispatch方法的函數。在第 1 箇中間件執行next時,相當於在執行dispatch(2),就進入到了下一個中間件的處理流程。因爲dispatch返回的都是Promise對象,因此在第 n 箇中間件await next()時,就進入到了第 n+1 箇中間件,而當第 n+1 箇中間件執行完成後,可以返回第 n 箇中間件。但是在某個中間件中,我們沒有寫next(),就不會再執行它後面所有的中間件。運行機制如下圖所示:

undefined

koa-convert 解析

在 koa2 中引入了 koa-convert 庫,在使用 use 函數時,會使用到 convert 方法(只展示核心的代碼):

const convert = require('koa-convert');

module.exports = class Application extends Emitter {
    use(fn) {
        if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
        if (isGeneratorFunction(fn)) {
            deprecate('Support for generators will be removed';
            fn = convert(fn);
        }
        debug('use %s', fn._name || fn.name || '-');
        this.middleware.push(fn);
        return this;
    }
}
複製代碼

koa2 框架針對 koa1 版本作了兼容處理,中間件函數如果是generator函數的話,會使用koa-convert進行轉換爲 “類 async 函數”。首先我們必須理解generatorasync的區別:async函數會自動執行,而generator每次都要調用 next 函數才能執行,因此我們需要尋找到一個合適的方法,讓next()函數能夠一直持續下去即可,這時可以將generatoryieldvalue指定成爲一個Promise對象。下面看看koa-convert中的核心代碼:

const co = require('co')
const compose = require('koa-compose')

module.exports = convert

function convert (mw) {
  if (typeof mw !== 'function') {
    throw new TypeError('middleware must be a function')
  }
  if (mw.constructor.name !== 'GeneratorFunction') {
    return mw
  }
  const converted = function (ctx, next) {
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  converted._name = mw._name || mw.name
  return converted
}
複製代碼

首先針對傳入的參數 mw 作校驗,如果不是函數則拋異常,如果不是generator函數則直接返回,如果是generator函數則使用co函數進行處理。co 的核心代碼如下:

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1);
  
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();
    
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
      return null;
    }

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    function next(ret) {
      if (ret.done) return resolve(ret.value);
      var value = toPromise.call(ctx, ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}
複製代碼

由以上代碼可以看出,co 中作了這樣的處理:

  1. 把一個generator封裝在一個Promise對象中

  2. 這個Promise對象再次把它的gen.next()也封裝出Promise對象,相當於這個子Promise對象完成的時候也重複調用gen.next()

  3. 當所有迭代完成時,對父Promise對象進行resolve

以上工作完成後,就形成了一個類 async 函數。

異步函數的統一錯誤處理機制

在 koa 框架中,有兩種錯誤的處理機制,分別爲:

  1. 中間件捕獲

  2. 框架捕獲

undefined

中間件捕獲是針對中間件做了錯誤處理響應,如fnMiddleware(ctx).then(handleResponse).catch(onerror),在中間件運行出錯時,會出發 onerror 監聽函數。框架捕獲是在context.js中作了相應的處理this.app.emit('error', err, this),這裏的this.app是對application的引用,當context.js調用onerror時,實際上是觸發application實例的error事件 ,因爲Application類是繼承自EventEmitter類的,因此具備了處理異步事件的能力,可以使用EventEmitter類中對於異步函數的錯誤處理方法。

koa 爲什麼能實現異步函數的統一錯誤處理?因爲 async 函數返回的是一個 Promise 對象,如果 async 函數內部拋出了異常,則會導致 Promise 對象變爲 reject 狀態,異常會被 catch 的回調函數 (onerror) 捕獲到。如果 await 後面的 Promise 對象變爲 reject 狀態,reject 的參數也可以被 catch 的回調函數 (onerror) 捕獲到。

委託模式在 koa 中的應用

delegates 庫由知名的 TJ 所寫,可以幫我們方便快捷地使用設計模式當中的委託模式,即外層暴露的對象將請求委託給內部的其他對象進行處理。

delegates 基本用法就是將內部對象的變量或者函數綁定在暴露在外層的變量上,直接通過 delegates 方法進行如下委託,基本的委託方式包含:

delegates 原理就是__defineGetter__和__defineSetter__。在 application.createContext 函數中,被創建的 context 對象會掛載基於 request.js 實現的 request 對象和基於 response.js 實現的 response 對象。下面 2 個 delegate 的作用是讓 context 對象代理 request 和 response 的部分屬性和方法:

undefined

做了以上的處理之後,context.request的許多屬性都被委託在context上了,context.response的許多方法都被委託在context上了,因此我們不僅可以使用this.ctx.request.xxthis.ctx.response.xx取到對應的屬性,還可以通過this.ctx.xx取到this.ctx.requestthis.ctx.response下掛載的xx方法。

我們在源碼中可以看到,response.js 和 request.js 使用的是 get set 代理,而 context.js 使用的是 delegate 代理,爲什麼呢?因爲 delegate 方法比較單一,只代理屬性;但是使用 set 和 get 方法還可以加入一些額外的邏輯處理。在 context.js 中,只需要代理屬性即可,使用 delegate 方法完全可以實現此效果,而在 response.js 和 request.js 中是需要處理其他邏輯的,如以下對 query 作的格式化操作:

get query() {
  const str = this.querystring;
  const c = this._querycache = this._querycache || {};
  return c[str] || (c[str] = qs.parse(str));
}
複製代碼

到這裏,相信你對 koa2 的原理實現有了更深的理解吧?

關於本文

作者:會喫魚的貓咪

https://juejin.cn/post/6966432934756794405

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/CAS2-5fGp6lDB6tF7DlYRg