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

一、中间件模式的基本原理
要实现中间件,首先要知道它的三个关键要素:一个存放处理函数的数组、一个触发执行的入口、以及一个控制流转的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