理解前端的 Middleware 原理与实现

前言

最早提出 Middleware 概念的是 Express,随后由原班人马打造的 Koa 不但沿用了 Middleware 的架构设计,还更加彻底的把自己定义为中间件框架。Redux也引入了 Middleware 的概念,方便独立功能的函数对 Action 进行处理。Axios虽然没有中间件,但其拦截器的用法却跟中间件十分相似。本文结合使用场景,拆解对比各大框架的 Middleware 的实现原理, 。

Middleware

Middleware(中间件)本意是指位于服务器的操作系统之上,管理计算资源和网络通信的一种通用独立的系统软件服务程序。分布式应用软件借助这种软件在不同的技术之间共享资源。

而大前端领域,Middleware 一般指提供通用独立功能的数据处理函数,包括日志记录、数据叠加和错误处理等。

Express

Express 中应用层级的中间件的注册方式:

const stack = [];
/** 通过 use 注册 */
function use(fn) {
  stack.push(fn);
}

/** 请求到达的时候,会触发handle方法。接着next函数从队列中顺序取出并执行 */
function handle(req, res) {
  var idx = 0;
  next();
  function next() {
    var fn = stack[idx++];
    fn(req, res, next)
  }
}

Koa

Koa的 Middleware 注册跟路由无关,所有的请求都会经过注册的中间件。同时Koa 支持async/await异步编程模式:

/** 注册 */
function use(fn) {
  // 省略部分代码...
  this.middleware.push(fn);
  return this;
}

Koa 的 Middleware 顺序执行,通过 dispatch函数来控制,compose 函数对已注册的中间件列表(middleware)栈内每一个中间件函数的校验,并返回 fn  函数。

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} ctx
   * @return {Promise}
   * @api public
   */
  return function fn (ctx, next) {
    return dispatch(0);
    function dispatch (i) {
      let middlewareFn = middleware[i]
      try {
        return Promise.resolve(middlewareFn(ctx, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  }
}

Redux

Redux中间件的参数经过柯里化,store是applyMiddleware内部传进来的,next是compose后传进来的,action是dispatch传进来的

export default function applyMiddleware(...middlewares) {
  return (createStore) =>(reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState)
    let dispatch = store.dispatch;
    let chain = [];
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    /** 先执行一遍middleware,把第一个参数store传进去 */
    chain = middlewares.map(middleware => middleware(middlewareAPI));
    /** 传入原始的dispatch */
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

这里 compose 的返回值又重新赋值给dispatch:

function compose (...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) =>(...args) => a(b(...args)))
}

说明我们在应用内调用的dispatch并不是store自带的,而是经过 Middleware 处理的升级版。

Axios

axios没有中间件,但有类似功能的拦截器(interceptors),本质上都是在数据处理链路的 2 点之间,提供独立的、配置化的、可叠加的额外功能。

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
function InterceptorManager() {
  this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

Axios内部会维护 2 个 interceptors,它们有独立的 handlers 数组。use就是往数组添加元素而已,跟其它框架不同的是这里的数组元素不是一个函数,而是一个对象,包含fulfilled和rejected 2 个属性。第二个参数不传的时候rejected就是 undefined。

通过 promise 的链式调用,将 interceptors 串联了起来,执行顺序是:requestInterceptorChain -> chain -> responseInterceptorChain。这里有一个默认的约定,chain 里的元素都是按照[fulfilled1, rejected1, fulfilled2, rejected2]这种模式排列的,所以注册 interceptors 的时候如果没有提供第二个参数,也会有一个默认值 undefined:

Axios.prototype.request = function request(config) {
  config = mergeConfig(this.defaults, config);
  // 成对的添加元素
  var requestInterceptorChain = [];
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  
  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  var chain = [dispatchRequest, undefined];
  
  Array.prototype.unshift.apply(chain, requestInterceptorChain);
  chain.concat(responseInterceptorChain);
  
  promise = Promise.resolve(config);
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
}

总结

这里面最精妙也是最难理解的就是Array.reduce这种形式,需要反复的推敲。promise.then链式调用的任务编排方法也十分巧妙,前面处理完的数据会自动传给下一个then。递归调用的形式则最好理解,Koa在Express实现的基础上天然支持异步调用,更符合服务器端场景 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定谔的猫96

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值