Koajs中介軟體之next(二)

petruslaw發表於2019-02-16

Koajs中介軟體之定義(一)
Koajs中介軟體之next(二)
Koajs中介軟體之context(三)

第一篇文章中我們講過,“在Koa中,中介軟體是指連貫整個 Koa 應用程式,並共享資源的獨立外掛”,注意兩個詞,“連貫”與“共享資源”,與上面的程式碼一一對應,“連貫”對應“next”,“共享資源對應context”。
Koa 中通過 next 貫穿整個應用程式,下面分析一下 next 中做了什麼。

中介軟體集合

Koa 類中的建構函式中初始化了一堆資料,其中兩個重要的,一個是“middleware”,另一個是“context”。(非關鍵程式碼使用…省略)

constructor() {
    ...
    this.middleware = [];
    this.context = Object.create(context);
    ...
  }

所有的中介軟體在一個陣列存放,當我們呼叫“app.use”方法的時候會往陣列中加入我們自定義的中間價

use(fn) {
    ...
    this.middleware.push(fn);
    return this;
}

最後通過“koa-compose”來統一觸發中介軟體佇列

callback() {
    const fn = compose(this.middleware);
    ...
    return (req, res) => {
      ...
      fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
    };
}

koa-compose

koa-compose 原始碼只有短短几十行,關鍵程式碼不到10行,直接貼上原始碼

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError(`Middleware stack must be an array!`)
  for (const fn of middleware) {
    if (typeof fn !== `function`) throw new TypeError(`Middleware must be composed of functions!`)
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    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
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

如果我們需要使用 Koa 的洋蔥模型可以直接呼叫 koa-componse 來達到目的

const koaCompose = require(`koa-compose`);

const middleware1 = (ctx, next) => {
    console.log(`middleware1 >>>>>`);
    next();
    console.log(`middleware1 <<<<<`);
}
const middleware2 = (ctx, next) => {
    console.log(`middleware2 >>>>>`);
    next();
    console.log(`middleware2 <<<<<`);
}

const middleware3 = (ctx, next) => {
    console.log(`middleware3 >>>>>`);
    console.warn(ctx);
    next();
    console.log(`middleware3 <<<<<`);
}

const fn = koaCompose([middleware1, middleware2, middleware3]);

fn({ a: `a` }, (ctx) => {
    console.warn(ctx);
    console.warn(`The last next use do =======<`);
    return ctx;
}).then((ctx) => {
    console.warn(ctx);
    console.warn(`end =====<`);
});

輸出:

middleware1 >>>>>
middleware2 >>>>>
middleware3 >>>>>
{ a: `a` }
{ a: `a` }
The last next use do =======<
middleware3 <<<<<
middleware2 <<<<<
middleware1 <<<<<
undefined
end =====<

精簡 koa-componse

為了更好的分析程式碼,去除 koa-componse 程式碼中的各種非關鍵判斷及非同步處理邏輯後,程式碼如下

const compose = function (middlewares) {
    // 返回一個函式,提供兩個引數,一個是傳入的上下文,另一個是所有中介軟體執行完畢後回撥
    return function(context, last) {
        let idx = -1;   // 初始定義當前執行中介軟體下標未-1,即表示當前未執行任何中介軟體
        
        
        return dispatch(0); // 手動觸發第1箇中介軟體

        function dispatch(i) {
            idx = i;    // 設定當前執行中介軟體下標
            let fn = middlewares[i] || last;    
            try {
                // 執行當前中介軟體的時候,給當前中介軟體引數中的next引數賦值為一個函式,在這個函式中執行下一個中介軟體
                if (fn) fn(context, function() {
                    dispatch(i + 1);    // 觸發下一個中間價,並且將中介軟體執行下標+1
                })
            } catch (err) { // 所有的中介軟體執行完畢,執行最後回撥
                last(context);
            }
        }
    }
};

執行程式碼:

const run = compose([middleware1, middleware2, middleware3]);

run({ a: `a` }, () => {
    console.warn(`Middlewares do last =======<`);
});

輸出:

middleware1 >>>>>
middleware2 >>>>>
middleware3 >>>>>
Middlewares do last =======<
middleware3 <<<<<
middleware2 <<<<<
middleware1 <<<<<

總體思路

  1. 將所有中介軟體推送到一個陣列中
  2. 第一個中介軟體首先執行
  3. 在第一個中介軟體執行前,將第一個中介軟體中的 next 引數設定為一個觸發下一個中介軟體執行的函式
  4. 第一個中介軟體呼叫 next 函式,把執行器交給下一個中間價
  5. 迴圈往復,直最後一箇中介軟體執行完畢
  6. 所有中介軟體執行完畢後,依次執行next外層程式碼

優點

  • 解決多重回撥地獄模式
  • 統一處理上下文context掛載與傳遞
  • 異常捕獲

缺點

  • 當一個專案中存在多箇中介軟體時,對於效能會有一定影響,對於優化來說是一種考驗

參考資料

Mdn 類
Koa 官網

相關文章