常用中間件原理淺析

常用中間件原理淺析

Koa2 中間件原理

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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) {
    // 記錄上一次執行中間件的位置
    let index = -1;
    return dispatch(0);
    function dispatch (i) {
      // 理論上 i 會大於 index,因爲每次執行一次都會把 i 遞增
      // 如果 小於或等於,則說明 next() 執行了多次
      // 比如在第一個中間件中 next() 執行兩次,此時,兩次 dispatch 接收的 i 值都是 2,第一次執行便會 將 2 賦值給 index,第二次執行則會命中 等於 判斷
      if (i <= index) return Promise.reject(new Error('next() called multiple times'));
      index = i;
      // 獲取當前的中間件
      let fn = middleware[i];
      // 最後一箇中間件中執行 next()
      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);
      }
    }
  }
}
  1. Koa 中間件串聯調用的核心邏輯在於 dispatch 函數中,每一箇中間件都可以拿到下一個中間件的執行手柄 next
  2. 每個中間件只能執行一次 next,執行多次會出現上面代碼中所說的問題
  3. 洋蔥圈模型如何實現 - 關鍵在 async await 的執行機制
  4. 中間件的基本結構如下
async function commonMiddleware(ctx, next){
  try{
    // do something
    await next()
    // do something
  }
  .catch(err){
    // handle err
  }    
}

Redux 中間件原理

先思考一個問題
如何將多個 平級函數 轉化爲從右到左的 聚合函數

比如

let fn = compose(fn1,fn2,fn3); // 輸出 (...args) => fn1(fn2(fn3(...args)))

// compose聚合函數的順序是從右到左
const compose = function (...funcs) {
  return funcs.reduce((fn1, fn2) => {
    return (...args) => {
      return fn1(fn2(...args));
    };
  });
}

看一下 Redux 中間件核心處理鏈式調用的函數

function applyMiddleware(...middlewares) {
  return (createStore) => (...args) => {
    const store = createStore(...args);
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      );
    };
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => {
        return dispatch(...args);
      },
    };
    // 這裏執行了一層中間件接收了{store.getState,dispatch}參數 
    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    // compose(...chain)(store.dispatch) 相當於fn1(fn2(fn3(store.dispatch)))
    // 又執行了一層中間件 這一層接收 next 參數 也就是下一個中間件參數
    dispatch = compose(...chain)(store.dispatch);
    return { ...store, dispatch };
  };
}

由以上中間件的處理方式可以看出,一箇中間件的基本結構如下

// 中間件邏輯代碼需要經過三次柯里化
store => next => action => {
  // 中間件邏輯代碼
  // do something
  next(action);
  // do something
}
  1. Action 派發後走同步修改操作,唯一有發揮空間的環節在 dispatch 環節
  2. Redux 中間件是對 dispatch 的增強(攔截處理),使其支持 函數、Promise 的處理

基於 Promise 鏈表實現中間件模塊

以上兩種方式都是可以在中間件內部手動調用 next 執行下一個中間件

有時候需要中間件列表是一個自動化的流水線,無需額外控制,自動全鏈路執行

比如下面的場景,需要中間件模塊順序執行一下

  1. 數據格式化 -> 數據編碼 -> 數據解析
  2. 數據格式化 -> UI組件解析器
const promiseMiddleware = (middlewares: any[], ctx: any) => {
  let promise = Promise.resolve(null);
  let next;

  // 1. 通過bind把執行上下文對象,綁定到中間件第一個參數
  middlewares.forEach((fn, i) => {
    middlewares[i] = fn.bind(null, ctx);
  });

  // 2. 通過while循環執行promise實例
  while ((next = middlewares.shift())) {
    promise = promise.then(next);
  }

  // 3. 最終返回一個promise實例結果
  return promise.then(() => {
    return ctx;
  });
}

統一中間件的調用流程

class MiddleWare  {
  middlewares = [];
  ctx = {
    message: {}
  }
  // 1. 構造器函數,初始化添加 middlewares
  constructor(middlewares) {
    super();
    this.middlewares = middlewares;
  }

  // 2. 通過批量添加中間件接口 
  useBatch(steps) {
    if (Array.isArray(steps)) {
      this.middlewares = this.middlewares.concat(steps);
    } else {
      throw TypeError('useBatch args must be an arrary!!!')
    }
  }

  // 3. 核心實現,每個Action都需要進過Dispatch進行觸發
  dispatch(message) {
    // 3.1 使用Object.create 創建新的 middlewares 和 ctx對象,防止對象引用
    let steps = Object.create(this.middlewares);
    let ctx = Object.create(this.ctx);
    // 3.2 賦值 消息
    ctx.message = message;
    // 3.3 執行中間件模塊,同時返回一個 promise 實例
    return promiseMiddleware(steps, ctx);
  }
}

基於 Promise 鏈式調用的還有 axios 的攔截器處理邏輯

總結

  1. 中間件的編寫邏輯需要結合業務進行取捨
  2. 核心邏輯在於如何處理中間件的鏈式調用(手動式、自動式)
  3. 對中間件列表加以封裝,對外提供統一的API調用

參考文章:

  1. Redux系列之分析中間件原理(附經驗分享)
  2. 50行代碼學會koa2中間件原理
  3. 從入門到深入:IM聊天系統前端開發實踐
  4. 前端網紅框架的插件機制全梳理(axios、koa、redux、vuex)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章