常用中间件原理浅析

常用中间件原理浅析

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