【Node】深入淺出 Koa 的洋蔥模型

本文將講解 koa 的洋蔥模型,我們爲什麼要使用洋蔥模型,以及它的原理實現。掌握洋蔥模型對於理解 koa 至關重要,希望本文對你有所幫助~

什麼是洋蔥模型

先來看一個 demo

const Koa = require('koa');
const app = new Koa();

// 中間件1
app.use((ctx, next) ={
    console.log(1);
    next();
    console.log(2);
});

// 中間件 2 
app.use((ctx, next) ={
    console.log(3);
    next();
    console.log(4);
});

app.listen(8000, '0.0.0.0'() ={
    console.log(`Server is starting`);
});

輸出的結果是:

1
3
4
2

koa 中,中間件被 next() 方法分成了兩部分。next() 方法上面部分會先執行,下面部門會在後續中間件執行全部結束之後再執行。可以通過下圖直觀看出:

在洋蔥模型中,每一層相當於一箇中間件,用來處理特定的功能,比如錯誤處理、Session 處理等等。其處理順序先是 next() 前請求(Request,從外層到內層)然後執行 next() 函數,最後是 next() 後響應(Response,從內層到外層),也就是說每一箇中間件都有兩次處理時機

爲什麼 Koa 使用洋蔥模型

假如不是洋蔥模型,我們中間件依賴於其他中間件的邏輯的話,我們要怎麼處理?

比如,我們需要知道一個請求或者操作 db 的耗時是多少,而且想獲取其他中間件的信息。在 koa 中,我們可以使用 async await 的方式結合洋蔥模型做到。

app.use(async(ctx, next) ={
  const start = new Date();
  await next();
  const delta = new Date() - start;
  console.log (`請求耗時: ${delta} MS`);
  console.log('拿到上一次請求的結果:', ctx.state.baiduHTML);
})

app.use(async(ctx, next) ={
  // 處理 db 或者進行 HTTP 請求
  ctx.state.baiduHTML = await axios.get('http://baidu.com');
})

而假如沒有洋蔥模型,這是做不到的。

深入 Koa 洋蔥模型

我們以文章開始時候的 demo 來分析一下 koa 內部的實現。

const Koa = require('koa');

//Applications
const app = new Koa();

// 中間件1
app.use((ctx, next) ={
  console.log(1);
  next();
  console.log(2);
});

// 中間件 2 
app.use((ctx, next) ={
  console.log(3);
  next();
  console.log(4);
});

app.listen(9000, '0.0.0.0'() ={
    console.log(`Server is starting`);
});

use 方法

use 方法就是做了一件事,維護得到 middleware 中間件數組

  use(fn) {
    // ...
    // 維護中間件數組——middleware
    this.middleware.push(fn);
    return this;
  }

listen 方法 和 callback 方法

執行 app.listen 方法的時候,其實是 Node.js 原生 http 模塊 createServer 方法創建了一個服務,其回調爲 callback 方法。callback 方法中就有我們今天的重點 compose 函數,它的返回是一個 Promise 函數。

  listen(...args) {
    debug('listen');
    // node http 創建一個服務
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

  callback() {
    // 返回值是一個函數
    const fn = compose(this.middleware);
    const handleRequest = (req, res) ={
      // 創建 ctx 上下文環境
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }

handleRequest 中會執行 compose 函數中返回的 Promise 函數並返回結果。

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    // 執行 compose 中返回的函數,將結果返回
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

koa-compose

compose 函數引用的是 koa-compose 這個庫。其實現如下所示:

function compose (middleware) {
  // ...
  return function (context, next) {
    // last called middleware #
    let index = -1
    // 一開始的時候傳入爲 0,後續會遞增
    return dispatch(0)
    function dispatch (i) {
      // 假如沒有遞增,則說明執行了多次
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      // 拿到當前的中間件
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      // 當 fn 爲空的時候,就會開始執行 next() 後面部分的代碼
      if (!fn) return Promise.resolve()
      try {
        // 執行中間件,留意這兩個參數,都是中間件的傳參,第一個是上下文,第二個是 next 函數
        // 也就是說執行 next 的時候也就是調用 dispatch 函數的時候
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

代碼很簡單,我們來看看具體的執行流程是怎樣的:

當我們執行第一次的時候,調用的是 dispatch(0),這個時候 i 爲 0,fn 爲第一個中間件函數。並執行中間件,留意這兩個參數,都是中間件的傳參,第一個是上下文,第二個是 next 函數。也就是說中間件執行 next 的時候也就是調用 dispatch 函數的時候,這就是爲什麼執行 next 邏輯的時候就會執行下一個中間件的原因:

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

當第二、第三次執行 dispatch 的時候,跟第一次一樣,分別開始執行第二、第三個中間件,執行 next() 的時候開始執行下一個中間件。

當執行到第三個中間件的時候,執行到 next() 的時候,dispatch 函數傳入的參數是 3,fnundefined。這個時候就會執行

if (!fn) return Promise.resolve()

這個時候就會執行第三個中間件 next() 之後的代碼,然後是第二個、第一個,從而形成了洋蔥模型。

其過程如下所示:

簡易版 compose

模範 koa 的邏輯,我們可以寫一個簡易版的 compose。方便大家的理解:

const middleware = []
let mw1 = async function (ctx, next) {
    console.log("next前,第一個中間件")
    await next()
    console.log("next後,第一個中間件")
}
let mw2 = async function (ctx, next) {
    console.log("next前,第二個中間件")
    await next()
    console.log("next後,第二個中間件")
}
let mw3 = async function (ctx, next) {
    console.log("第三個中間件,沒有next了")
}

function use(mw) {
  middleware.push(mw);
}

function compose(middleware) {
  return (ctx, next) ={
    return dispatch(0);
    function dispatch(i) {
      const fn = middleware[i];
      if (!fn) return;
      return fn(ctx, dispatch.bind(null, i+1));
    }
  }
}

use(mw1);
use(mw2);
use(mw3);

const fn = compose(middleware);

fn();

總結

Koa 的洋蔥模型指的是以 next() 函數爲分割點,先由外到內執行 Request 的邏輯,再由內到外執行 Response 的邏輯。通過洋蔥模型,將多箇中間件之間通信等變得更加可行和簡單。其實現的原理並不是很複雜,主要是 compose 方法。

參考

參考資料

[1]

Talk about koa’s onion model: https://developpaper.com/talk-about-koas-onion-model/

[2]

如何更好地理解中間件和洋蔥模型: https://juejin.cn/post/6890259747866411022

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