导读:本期聚焦于大象创作的《如何实现一个JavaScript的中间件模式,比如Express中的中间件?》,敬请观看详情。中间件是Node.js生态中最经典的设计模式之一,Express正是靠它构建起了灵活的请求处理体系。本文将从零开始剖析中间件的底层实现机制,讲解如何利用函数数组和next回调实现多个处理函数的链式调用,分析洋葱模型与线性模型的差异,并手写一个支持同步与异步中间件的迷你框架。文中还会涉及错误处理中间件的注册方式、中间件的执行顺序控制,以及async场景下的异常捕获技巧,帮助你真正理解Koa与Express在这一模式上的设计取舍。

中间件模式的核心思想其实很朴素:把一组处理函数按顺序放进一个队列,每个函数处理完自己负责的部分后,决定是继续传递控制权还是直接终止。Express正是基于这个模式,让路由、日志、鉴权、静态文件服务等功能得以像积木一样自由组合。理解它的实现原理,不仅能让你写出更可靠的Node.js代码,也能把这个模式迁移到前端拦截器、任务流水线等场景中。

如何实现一个JavaScript的中间件模式,比如Express中的中间件?

一、中间件模式的基本原理

要实现中间件,首先要知道它的三个关键要素:一个存放处理函数的数组、一个触发执行的入口、以及一个控制流转的next函数。Express中的app.use本质上就是往数组里push一个函数,而请求到来时,框架会从数组头部开始依次调用这些函数。

每个中间件函数都会接收三个参数:请求对象、响应对象和next回调。当一个中间件完成了自己的工作,比如打印日志或者解析请求体,它需要主动调用next()把控制权交给下一个中间件。如果它不调用next,也不结束响应,整个请求就会挂起,这正是新手常遇到的"接口一直pending"的原因。

下面用一个最简版本演示线性中间件的骨架,注意这里用索引来追踪当前执行到哪个函数,通过闭包把next传递下去:

class Middleware {
  constructor() {
    this.stack = []; // 存放所有中间件函数
  }
  use(fn) {
    this.stack.push(fn);
    return this; // 支持链式注册
  }
  run(req, res) {
    let index = 0;
    const next = () => {
      if (index >= this.stack.length) return; // 队列耗尽,结束
      const fn = this.stack[index++];
      fn(req, res, next);
    };
    next();
  }
}

const app = new Middleware();
app.use((req, res, next) => {
  console.log('第一个中间件');
  next();
});
app.use((req, res, next) => {
  console.log('第二个中间件');
  res.end('done');
});
app.run({}, {}); // 依次打印:第一个中间件、第二个中间件

这段代码不到三十行,却完整表达了Express中间件的核心:注册即入队,next即流转。Express在此基础上增加了路径匹配(app.use('/api', fn))和路由参数解析,但骨架完全一致。

二、异步中间件与错误处理的实现

真实的Web应用里,中间件几乎都是异步的:查数据库、调接口、读文件。而上面的同步实现有一个致命问题——如果中间件内部是异步的,next()会在异步操作完成前就被调用,导致后续中间件拿到不完整的数据。解决办法是把next的调用放进异步回调或者Promise的then链中,也可以用async函数配合await来保证顺序。

另一个重点是错误处理。Express约定,四参数的中间件(err, req, res, next)是错误处理中间件,只有调用了next(err)时才会被触发。框架内部通过try...catch捕获同步异常,并在Promise被reject时把错误传给错误中间件。下面的实现同时支持了异步链式调用和错误捕获:

async function runMiddleware(stack, req, res) {
  let index = 0;
  const next = async (err) => {
    if (err) throw err; // 有错误则向上抛
    if (index >= stack.length) return;
    const fn = stack[index++];
    await fn(req, res, next);
  };
  await next();
}

// 使用示例:异步中间件按顺序执行
const stack = [
  async (req, res, next) => {
    await new Promise(r => setTimeout(r, 100)); // 模拟异步操作
    req.user = { id: 1 };
    await next();
  },
  async (req, res) => {
    res.end('用户ID: ' + req.user.id);
  }
];

runMiddleware(stack, {}, {}).catch(err => {
  console.error('捕获到中间件错误:', err.message);
});

值得注意的是,async版本的next一旦reject,错误会沿着await链一路抛到最外层的catch。Koa正是采用这种基于async/await的实现,因此任何一个中间件抛出的异常都能被最外层统一处理,而不需要像Express那样专门注册四参数错误中间件。

三、线性模型与洋葱模型的区别

Express的中间件是线性模型:控制权像接力棒一样向后传递,中间件调用next()之后的代码基本不再有机会介入响应(因为响应可能已经结束了)。而Koa的洋葱模型则不同:每个中间件在await next()之后的代码,会在后续所有中间件执行完毕后逆序执行,形成"请求进入、响应离开"的完整闭环。

举个例子说明差异。假设有A、B两个中间件,Express中执行顺序是A前半段、B前半段、B结束、A后半段(如果A在next后还有代码);Koa中则是A前半段、B前半段、B后半段、A后半段。这使得Koa可以优雅地实现执行耗时统计、统一响应包装这类需求:

function compose(middlewares) {
  return function (ctx) {
    function dispatch(i) {
      const fn = middlewares[i];
      if (!fn) return Promise.resolve();
      return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
    }
    return dispatch(0);
  };
}

// 洋葱模型演示
const middlewares = [
  async (ctx, next) => {
    console.log('第一层进入');
    await next();
    console.log('第一层离开'); // 后续中间件全部完成后才执行
  },
  async (ctx, next) => {
    console.log('第二层进入');
    await next();
    console.log('第二层离开');
  }
];
compose(middlewares)({});
// 输出顺序:第一层进入、第二层进入、第二层离开、第一层离开

这种compose函数就是Koa核心源码koa-compose的简化版。它的精妙之处在于:next本身返回一个Promise,await它会等待整条后续链路完成,从而使每个中间件的"后半段"天然具备处理响应的能力。理解了这一点,你就明白了为什么Koa的日志、错误处理中间件总写在最外层,而路由逻辑放在内层。

四、实际应用中的设计建议

在自己实现中间件系统时,有几点经验值得参考。第一,注册接口要支持路径前缀匹配,这样可以让不同路由组挂载不同的处理链,Express的做法是把路径编译成正则再做前缀判断。第二,务必给中间件执行加上超时或异常兜底,避免某个中间件既不调用next也不结束响应,把连接拖死。

第三,对于纯同步场景,不要强行套用async/await,因为每次Promise创建都有微小开销,高并发下的累积成本不可忽视。可以像Express那样用try...catch处理同步异常,只在确有异步操作时才走Promise链。第四,中间件的执行顺序由注册顺序决定,这是隐式约定,容易出错,建议在团队规范中明确依赖型中间件(如body解析必须在业务处理之前)的注册位置,或者通过配置化的方式集中管理。

掌握中间件模式后,你会发现它远不止用于Web框架:axios的请求拦截器、Redux的中间件、构建工具的插件系统,本质上都是同一套"函数队列加控制权传递"的思想。理解原理,一套代码思路就能打通多种场景。

中间件模式JavaScriptExpress修改时间:2026-09-02 01:06:32

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。