让请求“排队”又“转身”:手写一个轻量中间件的实践之旅
在现代 Web 开发中,“中间件”这个词常被反复提及——它像一条看不见的流水线,串起身份校验、日志记录、错误处理、数据转换等各类横切逻辑。但对初学者而言,中间件常被抽象为框架内置的黑盒:调用 app.use()、写个函数、next() 一扔,就完事了。可它究竟是如何工作的?如果脱离 Express 或 Koa,我们能否从零构建一个真正可理解、可调试、可复用的中间件机制?
答案是肯定的。实现一个简单而典型的中间件系统,核心不在语法糖,而在三个关键设计:函数组合、执行顺序控制、上下文共享。
首先,明确中间件的本质:它是一组具有统一签名的函数,接收请求上下文(context)、可选的 next 函数(用于移交控制权),并可能修改 context 或提前终止流程。例如:
const logger = (ctx, next) => {
console.log(`→ ${new Date().toISOString()} ${ctx.method} ${ctx.path}`);
next();
};
const auth = (ctx, next) => {
if (!ctx.headers?.authorization) {
ctx.status = 401;
ctx.body = { error: 'Unauthorized' };
return; // 提前结束,不调用 next
}
next();
};
这里,ctx 是贯穿整个请求生命周期的共享对象,next 则是触发下一个中间件的“开关”。但单个函数无意义——真正的力量在于链式调度。
我们来手写一个 compose 函数,它接收中间件数组,返回一个可执行的“合成函数”:
function compose(middlewares) {
return function(ctx) {
let index = -1;
function dispatch(i) {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}
index = i;
const fn = middlewares[i];
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0);
};
}
这段代码看似简短,却承载了中间件运行的核心逻辑:

dispatch(i)递归调用自身,确保每个中间件仅执行一次;next()实际就是() => dispatch(i + 1),将控制权交予下一个;- 使用
Promise.resolve()统一异步处理,兼容同步与async/await中间件; index防止重复调用next(),避免逻辑错乱。
有了 compose,我们就能组装中间件链:
const app = compose([logger, auth, (ctx) => {
ctx.body = { message: 'Hello from middleware!' };
ctx.status = 200;
}]);
// 模拟一次请求
const ctx = { method: 'GET', path: '/api', headers: {} };
app(ctx).then(() => {
console.log('Response:', ctx.status, ctx.body);
});
运行时,控制台会先输出日志,再因缺少 authorization 头而返回 401——整个流程清晰可见,无需依赖任何框架。
这个简易中间件系统虽小,却已具备生产级中间件的骨架:
✅ 可插拔:任意符合 (ctx, next) => void | Promise 签名的函数均可加入;
✅ 可中断:通过不调用 next(),实现权限拦截、重定向等短路逻辑;
✅ 上下文透传:所有中间件共享同一 ctx,便于注入用户信息、数据库连接等;
✅ 错误冒泡:try/catch 与 Promise.reject 结合,使错误能逐层向上被捕获或统一处理。
当然,真实项目中还需补充超时控制、异常兜底、类型约束(如 TypeScript 接口定义)等能力。但这些优化,都建立在对“函数如何串联”这一本质的理解之上。
写到这里,或许你会意识到:所谓中间件,并非框架的恩赐,而是开发者对“关注点分离”这一古老原则的优雅践行——它把业务主干和通用逻辑解耦,让每个函数只专注一件事,再由一个精巧的调度器将其编织成网。
下一次你调用 app.use() 时,不妨暂停一秒,在心里默念那句 dispatch(0)。那不是魔法,而是你亲手写下的、最朴素也最有力的控制流。

发表评论