【Nodejs】1045- 一文搞懂koa2核心原理


koa的基礎結構

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

  • 它基於node原生req和res,封裝自定義的request和response對象,並基於它們封裝成一個統一的context對象。
  • 它基於async/await(generator)的洋蔥模型實現了中間件機制。

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(arguments1);
  
  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 方法進行如下委託,基本的委託方式包含:

  • getter:外部對象可以直接訪問內部對象的值
  • setter:外部對象可以直接修改內部對象的值
  • access:包含 getter 與 setter 的功能
  • method:外部對象可以直接調用內部對象的函數

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

聲明:文章著作權歸作者所有,如有侵權,請聯繫小編刪除。

1. JavaScript 重溫系列(22篇全)
2. ECMAScript 重溫系列(10篇全)
3. JavaScript設計模式 重溫系列(9篇全)
4.  正則 / 框架 / 算法等 重溫系列(16篇全)
5.  Webpack4 入門(上) ||  Webpack4 入門(下)
6.  MobX 入門(上)  ||   MobX 入門(下)
7. 120 +篇原創系列彙總

回覆“加羣”與大佬們一起交流學習~

點擊“閱讀原文”查看 120+ 篇原創文章

本文分享自微信公衆號 - 前端自習課(FE-study)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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