中间件链是把处理过程拆成可组合的小单元,按顺序执行并通过next传递控制。要做HelloWorld示例,关键是统一上下文、实现next调度、正确处理异步与错误,最后把中间件组合成可复用的执行管线。可以跨平台实现(如Node.js、浏览器),支持Promise/async,便于测试、调试与扩展,且稳定。

先说为什么:中间件链能解决什么问题
简单来说,中间件链就是把“大块”的请求或任务处理,拆成若干“小块”来做。每一块只负责一件事:日志、鉴权、输入校验、业务逻辑、异常处理等等。好处显而易见——职责单一,易于复用,顺序可控。你不必把所有逻辑塞进一个大函数,改起来也不会像拆炸弹那么小心翼翼。
中间件链的核心概念(用一句话解释一遍)
- 上下文(context):一个共享对象,承载请求数据、状态和最终响应。所有中间件读写同一个上下文,这就是它们沟通的方式。
- next 函数:把控制权传给下一个中间件的函数。调用 next() 表示“我做完了,你继续”。
- 单向/双向流:某些实现仅从上到下执行;而像 Koa 的洋葱模型,允许中间件在 next 之后继续执行,从而实现前后处理对称。
- 同步与异步:现实中中间件常包含异步 I/O(数据库、网络),所以必须正确支持 Promise/async。
一个类比帮助理解
把请求想象成在传送带上的包裹,每个工位(中间件)做一件事,做完呼叫下一站。当某个工位发现包裹损坏,它可以选择丢弃(结束链)或修复后传下去。这个模型让流程可视化,也易于排查。
HelloWorld 中间件链实战:一步步搭建
下面用最小实现来解释中间件链的关键步骤:注册、组合、执行。代码示例使用 JavaScript,目的在于说明概念,几乎可以直接移植到 Node.js 或浏览器环境。
1. 最简单的同步版(骨架)
思路:把中间件放到数组里,依次执行,并且每个中间件接收 context 和 next。
// 中间件函数签名示例: (ctx, next) => { ... }
// 同步 compose(仅作示意,不处理异步)
function composeSync(middlewares) {
return function (ctx) {
let i = 0;
function dispatch() {
const fn = middlewares[i++];
if (!fn) return;
fn(ctx, dispatch);
}
dispatch();
};
}
用法示例:
const mw1 = (ctx, next) => { ctx.log.push('mw1 start'); next(); ctx.log.push('mw1 end'); };
const mw2 = (ctx, next) => { ctx.log.push('mw2'); next(); };
const app = composeSync([mw1, mw2]);
const ctx = { log: [] };
app(ctx); // ctx.log => ['mw1 start','mw2','mw1 end']
注意:这个版本没有处理异步,也没有返回值或错误处理,仅用于理解控制流。
2. 支持异步的通用实现(常见的 compose)
现实中我们需要支持 Promise/async/await,并希望链式返回(便于在中间件后面继续处理)。下面就是 Koa 风格的 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);
};
}
这个实现解决了几个关键点:
- 防止多次调用 next()
- 通过 Promise 包装同步或异步中间件,使得 await app(ctx) 可用
- 使得中间件在调用 await next() 后仍可继续执行,形成“洋葱模型”
3. 带错误处理与超时的演进
在生产中,错误和超时是常态。一个中间件链应当提供统一的错误捕获与超时保护。
async function runWithTimeout(p, ms) {
let timer;
const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('timeout')), ms); });
try {
const res = await Promise.race([p, timeout]);
return res;
} finally {
clearTimeout(timer);
}
}
// 在调用 compose 返回的函数时包裹超时与全局错误捕获
async function safeRun(app, ctx, opts = { timeout: 5000 }) {
try {
await runWithTimeout(app(ctx), opts.timeout);
} catch (err) {
// 全局错误处理,写日志或设置 ctx.error 等
ctx.error = err;
}
}
4. 示例:实现一个 HelloWorld 服务管道
把几段中间件串起来:日志、身份、业务、响应。每段只做一件事。
const mwLog = async (ctx, next) => {
ctx.start = Date.now();
console.log('start', ctx.reqId);
await next();
console.log('end', ctx.reqId, 'spent', Date.now() - ctx.start);
};
const mwAuth = async (ctx, next) => {
if (!ctx.headers || !ctx.headers.authorization) {
ctx.res = { status: 401, body: 'Unauthorized' };
return; // 不调用 next,短路
}
ctx.user = { id: 1, name: 'alice' };
await next();
};
const mwHello = async (ctx, next) => {
// 业务逻辑
ctx.res = { status: 200, body: `Hello, ${ctx.user ? ctx.user.name : 'guest'}` };
await next();
};
const app = compose([mwLog, mwAuth, mwHello]);
// 执行
const ctx = { reqId: 'r123', headers: { authorization: 'token' } };
await app(ctx);
console.log(ctx.res);
上面的短路行为是必须的:鉴权失败直接返回 401,不再执行后续业务。
中间件设计要点与可扩展技巧
- 统一上下文:把请求/响应/状态放到 ctx,避免使用全局变量。
- 保持中间件无副作用:除了 ctx,尽量不要操作外部可变状态。
- 返回约定:中间件应当返回 Promise(或值),便于 compose 统一 await。
- 短路与恢复:中间件可以选择不调用 next 以短路流,也可以捕获下游异常后做补救。
- 幂等与重入:确保中间件在重复执行时不会对外部资源造成不可恢复的变化。
中间件优先级与顺序的直觉
通常把“前置”的、与请求校验相关的放在链的前面(日志、限流、鉴权、验证),把“后置”的渲染或响应处理放在后面。洋葱模型让你可以在前置做准备,在 next() 返回后清理或补记录。
对比表:Express / Koa / 自实现
| 框架 | 中间件模型 | 特点 |
| Express | 基于回调 (req, res, next) | 简单、广泛兼容,但不原生支持 async/await 洋葱式流程 |
| Koa | Promise/async 洋葱模型 (ctx, next) | 支持 await next() 后续处理,写法清晰 |
| 自实现 | 可定制(同步/异步/混合) | 灵活,适合嵌入到特殊平台或做轻量化服务 |
测试、调试与性能注意
- 单元测试:把中间件当成纯函数测试,构造一个最小 ctx,断言 ctx 被正确修改或 short-circuit。
- 集成测试:组合若干中间件后,模拟真实请求的上下文,验证顺序和异常流。
- 性能:中间件链过长会带来函数调用开销;避免在热路径做大量同步计算或不必要的 await。
- 剖析:用日志记录 start/end 时间点,识别慢中间件并进行优化或并行化(如果可行)。
常见陷阱与实用建议(写给会经常改代码的你)
- 不要多次调用 next():这会破坏控制流,compose 常检查并抛错。
- 谨慎短路:短路虽然方便,但越多短路会让流程难以预判,建议把短路放在显式的验证或错误处理中。
- 错误传播策略:决定是让错误往上抛还是在中间件内部统一处理,一旦选定就保持一致。
- 避免 ctx 过大:上下文包含太多字段会导致耦合,给 ctx 设计清晰的命名与结构。
- 中间件可组合性:把通用功能抽成独立中间件,使用工厂函数(如 createAuth(options))提升复用。
扩展场景:条件分支、子管道与中间件组合器
有时候希望按条件执行不同管道,或者把一组中间件视作子模块,这时可以做“分支中间件”或“子管道函数”。
// 条件中间件示意
const conditional = (predicate, trueMiddleware, falseMiddleware) => {
return async (ctx, next) => {
if (predicate(ctx)) {
await trueMiddleware(ctx, next);
} else {
await falseMiddleware(ctx, next);
}
};
};
此外,提供中间件组合器(高阶函数)可以把中间件库拼接成可复用的片段:
const group = (...mws) => compose(mws);
const authGroup = group(rateLimit, auth, attachUser);
在不同运行环境下的适配
中间件链不是 Node.js 的专利。浏览器中的事件处理、桌面应用的插件管线、服务器端函数的处理流水线都可以使用同样的模式。关键是:提供一个可移植的 compose,和一致的 ctx 约定。
真实案例摘录(便于参考)
- Express 的中间件是以 req/res 为中心,适合传统 HTTP 处理。
- Koa 的中间件鼓励使用 ctx,并利用 async/await 实现“前置/后置”一体化逻辑。
- 很多微服务框架在内部也把请求处理实现成 Pipeline,用于统一的拦截日志、鉴权、限流等。
如果你现在要把一个简单的 HelloWorld 中间件链上线,建议先用上面的 compose 实现做一个小原型:先保证同步逻辑正确,再逐步把异步、超时、错误处理补齐。测试方面,先写单元测试覆盖短路、异常和顺序,然后做一次压力测试看看延迟分布。
写到这里,我突然想到一个细节:很多人把中间件当作黑盒直接复用,却忘了版本兼容。记得给中间件写清楚约定(ctx 字段说明、是否会短路、是否抛错),这样团队协作会顺很多。好,那就这样,你可以把上面的代码直接拿去试试,调整 ctx 字段名或返回约定,适配到你的运行环境里。希望这篇“边写边想”的教程对你动手实现 HelloWorld 中间件链有实际帮助。