开源的中间件调度

当下流行的轮子库,都会提供强大拓展能力。

通常这种拓展能力以调度中间件或者拦截器的形式存在。

为了方便下面统称这类拓展程序能力的代码叫做中间件。

如何能写出一个拓展性强的轮子?

通过学习开源库的代码来学习如何写一个符合自己轮子的中间件。

核心概念

计算机的本质就是进行数据输入和输出,千百年来这个本质一直没有变过,通过中间对数据的处理,获取到想要的数据结果。

数据输入与输出

比如传入的数据可能是 {a:'hello'} 通过中间件的层层作用,就可能变成 {a:'h_e_l_l_o'}

或者对于koa的``context`提供访问数据库的能力。

这些都是对中间过程进行了拓展,掌握中间件的思维方式能够让我们的程序可拓展性更强。

下面将介绍以下两种中间件。

  • 直接对核心对象或者数据进行拓展。
  • 中间件去控制下一个中间件的执行(洋葱模型)。

对于核心数据拓展

axios 拦截器调度机制

axios 拦截器就是典型的修改数据的中间件

它的拦截器对请求前的数据和请求后的数据分别做了处理。

来看看下面的代码吧。

// 为了方便讲解摘取其核心逻辑
// 具体代码位置
// https://github.com/axios/axios/blob/master/lib/core/Axios.js#L27
function request(config) {
  // 初始化中间件,dispatchRequest 就是把 request -> response的过程
  // 通过调用 xhr 或者 node 的http 把请求数据变更为 response
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 把 request 的拦截器,插入到 chain 队头,把 response 的拦截器插入到队尾
  // 最后生成这样的数组 [requestInterceptor,...., dispatchRequest , .... responseInterceptor]
  this.interceptors.request.forEach(function unshiftRequestInterceptors(
    interceptor
  ) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(
    interceptor
  ) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // 从 requestInterceptor 执行到 responseInterceptor
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
}

图解执行流程

axios 拦截器执行

可以看出 axios 的中间件机制,是通过处理 request config 的数据,(可以修改所有的请求类型为 post,为所以的请求添加请求前缀,还可以根据请求配置设置全局 spinning)修改返回的 response 数据 (可以完成请求的数据 mock,变更字段名等)完成的。

当到用户手中的时候,经历层层中间件对数据的处理。

我们再来看看下一个针对数据进行拓展的开源库。

markdown-it 的中间件调度机制

markdown-it 的中间件相对于 axios 不那么相同,它具备认领机制,如果当前中间件对数据进行认领就代表当下的数据归这个中间件进行处理,进而跳过其他中间件的处理。相同点是把所有的中间件注册一个数组中。

我们了解 markdown-it 的中间件,我们无需详细了解 markdown-it 的 token 是怎么工作的,我们只需要知道 token 会渲染成不同的 html 标签就可以。

markdown-it 对文本进行 行 处理,对每行文本都会对中间件数组进行遍历,比如 遇见 ## 会被 heading 中间件进行处理,然后 heading 对这个#进行认领,并返回 true ,其他数组就不会处理这行文字了。

markdown-it 中间件

//https://github.com/markdown-it/markdown-it/blob/master/lib/parser_block.js#L48
// Generate tokens for input range
ParserBlock.prototype.tokenize = function (state, startLine, endLine) {
 var ok, i,
     rules = this.ruler.getRules(''),
     len = rules.length,
     line = startLine,

 while (line < endLine) {

   if (line >= endLine) { break; }

   // 每行都会重复遍历所有规则,如果规则认领,就会跳过其他规则处理。
   for (i = 0; i < len; i++) {
     ok = rules[i](state, line, endLine, false);
     // 当规则返回true的时候,会跳过其他规则
     if (ok) { break; }
   }

   // 对line操作不当可能会导致死循环。
   line = state.line;

 }
};

通过上面的展示可以看出我们对于数据的拓展,都是传入核心数据,对核心数据进行增删改查操作。

这种主要针对数据做处理的方式可以应用在:请求库、文本处理库(编译模板的插件),图片处理库(可以对图片上传前文件信息水印处理等偏重于数据处理的轮子上。

中间件实战

了解了这些我们可以用一行代码写出我们自己的中间件拓展。

首先我们知道我们的输入是一个数据,中间经过层层过滤,下一个中间件的输入是上一个中间件的输出。

js 中的 reduce 是一个会接受前面输入产生新的输出的函数,我们可以用它做中间件调度的载体,设置初始数据,遍历每个中间件对数据进行处理。

有了这个思路之后代码就浮现在脑子里了。大家来看。

const middleware = [v=>v+1,v=>v+2,v=>({message:`v 的值是 ${v})`})]

const result = middleware.reduce((res,fn)=>fn(res),0);

// output
// {message: "v 的值是 3"}

通过 reduce 我们快速的实现了对于数据处理了中间件拓展功能,其核心思想就是中间件对层层数据进行操作,最后输出想要的结果。

中间件去控制下一个中间件的执行

这种中间件通过调用下一个中间件进行拓展,是否放过当前请求到下一个中间件,取决于当前的中间件是否通过 next 方法调用下一个中间件。

koa 就是这样实现其中间件的,对于 node 的 http 进行拓展,对全局对象 context 进行拓展,挂载其他常用对象(request,response)到全局对象上,我们也可以通过这种方式把日志对象和数据库对象挂载到全局对象中进行对 koa 功能的拓展。

koa 中间件的调度机制

那么他是怎么做到的呢?

koa中间件调度

从图中可以看到只有调用 next 才会调用下一个中间件对数据进行处理,如果不调用那么就不会请求下一个中间件的执行。

但是这种方式也是存在缺点的,因为这样套娃的方式调用,会影响垃圾回收,因为下一个next不执行完毕就不会释放next() 之前的代码

最后来看它的代码吧

// https://github.com/koajs/compose/blob/master/index.js#L19
// 首先传入中间件数组 [middleware...]
function compose(middleware) {
  // 返回一个新的方法,调用这个方法就会执行所有中间件。
  return function (context, next) {
    // last called middleware #
    let index = -1;
    return dispatch(0);

    function dispatch(i) {
      // 如果一个中间件内调用 next 多次就会导致多次 i+1
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      // 当前中间件
      let fn = middleware[i];
      // 如果是中间件遍历到了最后一个
      if (i === middleware.length) fn = next;
      // 最后的next
      if (!fn) return Promise.resolve();
      try {
        // 调用当前中间件,并传入context和下一个中间件
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

// 使用

async function demo() {
  const middleware = [
    (c, n) => {
      c.v += 1;
      return n();
    },
    (c, n) => {
      c.v += 2;
      return n();
    },
    (c, n) => {
      return { message: `v 的值是 ${c.v}` };
    },
  ];

  const start = compose(middleware);

  const result = await start({ v: 0 });

  console.log(result);
}

demo();

上面就是现在流行的三个开源库对于中间件的写法喽,你学会了吗?