编码母题 / P0 / Node 与 JavaScript / 01

洋葱模型中间件组合器

先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。

重要程度必练难度中等预计完成30–40 分钟

难度按 LeetCode 简单/中等/困难三档估计;工程题并非 LeetCode 官方题目。用时指独立编码并检查主要边界的参考时间,因经验而异。

题目是什么

实现洋葱式中间件 compose。每层可在 await next() 前后执行逻辑;异常向外传播,同一层不得重复调用 next。

输入输出示例

A 前、B 前、B 后、A 后是两个中间件的典型执行顺序。

练习起手式与验证

复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。

/*
 * 题目:洋葱模型中间件组合器
 * 要求:实现洋葱式中间件 compose。每层可在 await next() 前后执行逻辑;
 * 异常向外传播,同一层不得重复调用 next。
 * 示例:A 前、B 前、B 后、A 后是两个中间件的典型执行顺序。
 */
function compose(middleware) {}

(async () => {
  const order = [];
  const fn = compose([
    async (_, next) => {
      order.push(1);
      await next();
      order.push(6);
    },
    async (_, next) => {
      order.push(2);
      await next();
      order.push(5);
    },
    async () => {
      order.push(3);
      await Promise.resolve();
      order.push(4);
    },
  ]);
  await fn({});
  console.log(order); // 预期:[1, 2, 3, 4, 5, 6]
  console.log(await Promise.resolve(compose([
      async (_, next) => {
        await next();
        await next();
      },
    ])({})).then(() => false, () => true)); // 预期:true(拒绝)
  console.log(await Promise.resolve(compose([
      () => {
        throw new Error("boom");
      },
    ])({})).then(() => false, () => true)); // 预期:true(拒绝)
})().catch(console.error);

解题思路

展开思路、不变量与常见边界

关键切入点:dispatch(index) 返回 Promise 并推进调用链;索引只增不退,重复 next 必须拒绝。

核心模型:dispatch(index) 递归返回 Promise。index 只能严格递增;若当前 index 不大于已执行索引,说明 next 被重复调用。时间 O(n),异步调用链空间 O(n)。 常见错误:不 return/await next 导致外层提前结束;漏包 Promise.resolve;允许 next 多次推进同一链。迁移:Koa、拦截器、Agent Hook、Tool 中间件。

参考代码与验证

“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。

// 关键机制:dispatch(index) 返回 Promise 并推进调用链;索引只增不退,重复 next 必须拒绝。
function compose(middleware) {
  if (
    !Array.isArray(middleware) ||
    !middleware.every((fn) => typeof fn === "function")
  )
    throw new TypeError("middleware must be functions");
  return function execute(context, next) {
    let lastIndex = -1;
    function dispatch(index) {
      if (index <= lastIndex)
        return Promise.reject(new Error("next() called multiple times"));
      lastIndex = index;
      const fn = index === middleware.length ? next : middleware[index];
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, () => dispatch(index + 1)));
      } catch (error) {
        return Promise.reject(error);
      }
    }
    return dispatch(0);
  };
}

// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
  const order = [];
  const fn = compose([
    async (_, next) => {
      order.push(1);
      await next();
      order.push(6);
    },
    async (_, next) => {
      order.push(2);
      await next();
      order.push(5);
    },
    async () => {
      order.push(3);
      await Promise.resolve();
      order.push(4);
    },
  ]);
  await fn({});
  console.log(order); // 预期:[1, 2, 3, 4, 5, 6]
  console.log(await Promise.resolve(compose([
      async (_, next) => {
        await next();
        await next();
      },
    ])({})).then(() => false, () => true)); // 预期:true(拒绝)
  console.log(await Promise.resolve(compose([
      () => {
        throw new Error("boom");
      },
    ])({})).then(() => false, () => true)); // 预期:true(拒绝)


})().catch(console.error);

复杂度分析

时间复杂度:O(m)。调用链按顺序进入和退出 m 层中间件各一次。

空间复杂度:O(m)。嵌套异步调用链深度最多 m 层。

下载原始 JS 文件