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

本文將講解 koa 的洋蔥模型,我們?yōu)槭裁匆褂醚笫[模型,以及它的原理實(shí)現(xiàn)。掌握洋蔥模型對于理解 koa 至關(guān)重要,希望本文對你有所幫助~

什么是洋蔥模型

先來看一個 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`);
});

輸出的結(jié)果是:

1
3
4
2

koa 中,中間件被 next() 方法分成了兩部分。next() 方法上面部分會先執(zhí)行,下面部門會在后續(xù)中間件執(zhí)行全部結(jié)束之后再執(zhí)行??梢酝ㄟ^下圖直觀看出:

在洋蔥模型中,每一層相當(dāng)于一個中間件,用來處理特定的功能,比如錯誤處理、Session 處理等等。其處理順序先是 next() 前請求(Request,從外層到內(nèi)層)然后執(zhí)行 next() 函數(shù),最后是 next() 后響應(yīng)(Response,從內(nèi)層到外層),也就是說每一個中間件都有兩次處理時機(jī)。

為什么 Koa 使用洋蔥模型

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

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

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

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

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

深入 Koa 洋蔥模型

我們以文章開始時候的 demo 來分析一下 koa 內(nèi)部的實(shí)現(xiàn)。

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 方法就是做了一件事,維護(hù)得到 middleware 中間件數(shù)組

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

listen 方法 和 callback 方法

執(zhí)行 app.listen 方法的時候,其實(shí)是 Node.js 原生 http 模塊 createServer 方法創(chuàng)建了一個服務(wù),其回調(diào)為 callback 方法。callback 方法中就有我們今天的重點(diǎn) compose 函數(shù),它的返回是一個 Promise 函數(shù)。

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

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

handleRequest 中會執(zhí)行 compose 函數(shù)中返回的 Promise 函數(shù)并返回結(jié)果。

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

koa-compose

compose 函數(shù)引用的是 koa-compose 這個庫。其實(shí)現(xiàn)如下所示:

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

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

當(dāng)我們執(zhí)行第一次的時候,調(diào)用的是 dispatch(0),這個時候 i 為 0,fn 為第一個中間件函數(shù)。并執(zhí)行中間件,留意這兩個參數(shù),都是中間件的傳參,第一個是上下文,第二個是 next 函數(shù)。也就是說中間件執(zhí)行 next 的時候也就是調(diào)用 dispatch 函數(shù)的時候,這就是為什么執(zhí)行 next 邏輯的時候就會執(zhí)行下一個中間件的原因:

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

當(dāng)?shù)诙?、第三次?zhí)行 dispatch 的時候,跟第一次一樣,分別開始執(zhí)行第二、第三個中間件,執(zhí)行 next() 的時候開始執(zhí)行下一個中間件。

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

if (!fn) return Promise.resolve()

這個時候就會執(zhí)行第三個中間件 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();

總結(jié)

Koa 的洋蔥模型指的是以 next() 函數(shù)為分割點(diǎn),先由外到內(nèi)執(zhí)行 Request 的邏輯,再由內(nèi)到外執(zhí)行 Response 的邏輯。通過洋蔥模型,將多個中間件之間通信等變得更加可行和簡單。其實(shí)現(xiàn)的原理并不是很復(fù)雜,主要是 compose 方法。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 中間件概念在編程中使用廣泛, 不管是前端還是后端, 在實(shí)際編程中或者框架設(shè)計(jì)都有使用到這種實(shí)用的模型, 下面我們就...
    折枝贈遠(yuǎn)方閱讀 1,082評論 0 1
  • koa-compose:koa-compose則是將 koa/koa-router 各個中間件合并執(zhí)行,結(jié)合 ne...
    貝程學(xué)院_前端閱讀 3,358評論 1 4
  • 分析 1、首先這是koa2最簡單的入門例子,我將通過這個入門例子來演示koa2的洋蔥模型 在這里面,app首先是調(diào)...
    隔壁老王的隔壁啊閱讀 3,854評論 0 1
  • 前幾天面試node,面試官問了koa的中間件是如何實(shí)現(xiàn)的,我一想,臥槽,這特么不是我很熟悉的么,然后就哇啦啦啦的一...
    書生逛酒家閱讀 616評論 0 0
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 7,573評論 0 4

友情鏈接更多精彩內(nèi)容