50行代码串行Promise,koa洋葱模型原来是这么实现?

756 阅读2分钟

1. Koa 中间件

Koa 的中间件通过一种更加传统的方式进行级联,摒弃了以往 node 频繁的回调函数造成的复杂代码逻辑。使用异步函数,我们可以实现"真正" 的中间件。与之不同,当执行到 yield next 语句时,Koa 暂停了该中间件,继续执行下一个符合请求的中间件('downstrem'),然后控制权再逐级返回给上层中间件('upstream')。(koa中文文档的描述

洋葱模型.png

koa的文档上有个非常代表性的中间件执行 gif 图。

e24be203325449ba902606c735b03a9e~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.gif

2. Koa Compose 源码解析

2.1 test.js测试compose的代码,先对compose的使用有个了解

//test.js
function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms || 1));
}

function isPromise(x) {
  return x && typeof x.then === "function";
}

describe("Koa Compose", function () {
  it.only("should work", async () => {
    const arr = [];
    const stack = [];
    //这里删除了部分测试代码
    stack.push(async (context, next) => {
      arr.push(1);
      await wait(1);
      await next(); //dispatch(1)
      await wait(1);
      arr.push(6);
    });

    stack.push(async (context, next) => {
      arr.push(2);
      await wait(1);
      await next(); //dispatch(2)
      await wait(1);
      arr.push(5);
    });
//调用compose方法,传入异步任务队列,执行compose返回
    await compose(stack)({});
    expect(arr).toEqual(expect.arrayContaining([1, 2, 5, 6]));
  });
 });

2.2. compose源码解析


function compose(middleware) {
  if (!Array.isArray(middleware))
    // 判断middleware是否是数组,判断数组的方法能想到几种呢
    throw new TypeError("Middleware stack must be an array!");
  for (const fn of middleware) {
    // 判断middleware里面的数据是否是函数, 顺便了解下for of 语法
    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)
        //如果i==index说明,同一个dispatch(fn里面的next)调用了多次,抛出错误
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) {
        // compose(stack)({}),没有传递第二个参数,所以next=undefined
        fn = next;
      }
      if (!fn) {
        return Promise.resolve();
      }
      try {
        //这里取middleware的函数去执行,函数内部执行next(),执行middleware的下一个函数
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}
/*
测试脚本的调用流程
dispatch(0) 
调用stack[0]的方法, 内部next()调用的是 dispach(1)
  arr=[1]
  dispach(1) 
调用stack[1], next= dispatch(2)
  arr=[1,2]

当2==middleware.length ,compose(stack)({}),没有传递第二个参数,所以next=undefined
后面的就是顺序执行
  await wait(1);
  arr.push(5);
  await wait(1);
  arr.push(6);
*/

2.3. vsCode中debug方法截图

在package.json里找的script,有有Debug的按钮,点击选择执行test image.png

扩展: redux中compose的代码

function compose(...funcs: Function[]) {
  if (funcs.length === 0) {
    // infer the argument type so it is usable in inference down the line
    return <T>(arg: T) => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce(
    (a, b) =>
      (...args: any) =>
        a(b(...args))
  )
}

3.小结

koa-compose源码比较少,调试了好几遍才理解执行流程和原理。其中包含着闭包、Promise、bind等基础知识。 熟悉了 koa-compose 中间件常说的洋葱模型,学会了部分 jest 用法,如果单独执行某一个单元测试,可以使用it.only()。

it.only('should work', async () => {
    //xxxx
})