一、为什么需要中间件/管道链
你写了一个 HTTP 服务,最初只有业务逻辑。后来要加日志,就在处理函数开头写几行;要加认证,又在开头加一段校验;要加限流,再加一段计数逻辑。三个月后,业务逻辑被埋在几十行横切代码中间,改一个地方要翻半天。
更麻烦的是顺序问题。如果限流在认证之前运行,未认证的请求也会消耗限流配额——攻击者发一堆无效请求就能耗尽合法用户的配额。这种 bug 不是逻辑错误,而是代码组织方式导致的隐含依赖。
中间件/管道链把每个关注点抽成独立的中间件,按顺序串成链条。每个中间件只做一件事,然后调用 next() 把控制权交给下一个。不调用 next() 就短路整条链。请求向内流入,响应向外流出,形成「洋葱模型」。
二、现实类比
机场安检通道。你的行李先过 X 光机(日志记录),然后过金属探测器(身份认证),再查验证件(参数校验)。每个关卡只做一件事,然后送你到下一个。任何一个关卡都可以拒绝你通过——相当于不调用 next() 短路整条链。
三、核心思想
每个中间件接收一个上下文和一个 next() 函数。调用 next() 将控制传递给链中下一个中间件;next() 返回后,中间件可以运行后处理逻辑。不调用 next() 则短路整个链。这创建了一个双向管道——请求向内流入,响应向外流出。
执行顺序:A.pre → B.pre → C.pre → C.post → B.post → A.post。即使 B 短路(不调用 next()),A 的后处理仍然会执行——这就是为什么日志中间件即使对被拒绝的请求也能正确记录。
| 属性 | 值 |
|---|---|
| 组合 | 每请求执行 O(n) 个中间件 |
| 短路 | 任何中间件可通过不调用 next() 跳过后续 |
| 上下文共享 | 所有中间件共享同一个可变上下文对象 |
| 方向 | 双向——进入时前处理,返回时后处理 |
四、变体与对比
| 模式 | 关系 | 区别 |
|---|---|---|
| 观察者(Observer) | 中间件可以观察和修改流经管道的请求/响应 | 观察者是一对多广播,中间件是一对一链 |
| 迭代器(Iterator) | 中间件链像迭代器遍历序列 | 迭代器只读遍历,中间件可修改和短路 |
| 注册表(Registry) | 注册表可以存储和管理链中的中间件 | 注册表管发现,中间件管执行流 |
| 虚函数表(Vtable) | 每个中间件是实现通用接口的函数指针 | Vtable 是类型分发,中间件是顺序组合 |
五、多语言实现
5.1 Go 实现
package pipeline
// Handler 是管道终端的处理函数type Handler func(ctx map[string]any)
// Middleware 是中间件函数,接收上下文和下一个处理器type Middleware func(ctx map[string]any, next Handler)
// Chain 将多个中间件组合为单一处理器func Chain(middlewares ...Middleware) Handler { return func(ctx map[string]any) { var run func(i int) run = func(i int) { if i < len(middlewares) { // 将后续链包装为 next handler middlewares[i](ctx, func(c map[string]any) { run(i + 1) }) } } run(0) }}使用示例:
func logging(ctx map[string]any, next Handler) { start := time.Now() next(ctx) // 调用下游 fmt.Printf("耗时: %v\n", time.Since(start))}
func auth(ctx map[string]any, next Handler) { token, _ := ctx["token"].(string) if token == "" { ctx["error"] = "未认证" // 短路:不调用 next return } next(ctx)}
// 组合:日志 → 认证 → 业务处理handler := Chain(logging, auth, func(ctx map[string]any) { ctx["result"] = "成功"})5.2 TypeScript 实现
// 中间件类型:接收上下文和 next 函数type Middleware<T> = (ctx: T, next: () => void) => void;
class Pipeline<T> { private middlewares: Middleware<T>[] = [];
// 添加中间件到链尾 use(middleware: Middleware<T>): void { this.middlewares.push(middleware); }
// 执行中间件链 execute(ctx: T): void { let index = 0; const next = (): void => { if (index < this.middlewares.length) { const mw = this.middlewares[index]!; index++; mw(ctx, next); } }; next(); }}关键实现细节:next() 通过闭包捕获 index 变量,每次调用时递增并执行下一个中间件。不调用 next() 就自然短路。这种模式比递归构建函数栈更节省内存,也更容易理解。
六、生产验证
- gRPC-Go — server.go#L1224-L1260 中的
chainUnaryServerInterceptors将拦截器链接为单一处理器,getChainUnaryHandler递归构建链,用于生产 gRPC 服务中的认证、日志、追踪和限流。 - Koa.js — application.js#L152-L204 中
use()将中间件推入数组,callback()通过koa-compose组合为单一函数。Koa 开创了异步洋葱模型,每个await next()创建一个栈帧,使下游中间件可以使用try/catch/finally。 - Express.js —
app.use()链接中间件用于 HTTP 请求处理,是 Node.js 生态最广泛使用的中间件实现。
七、小结
何时使用:
- HTTP 请求处理——认证、日志、CORS、压缩、限流作为可组合层
- RPC 拦截器——gRPC 拦截器用于追踪、认证、重试和指标
- 构建/编译管道——Webpack loader、Babel 转换各自处理后传递给下一个
- CLI 命令处理——参数解析、验证、帮助生成作为中间件
何时不用:
- 事件扇出(一对多)——需要多个独立处理器响应同一事件时,用观察者模式
- 无状态转换——每步只是转换数据不需要包裹下一步时,用
array.map().filter().reduce() - 性能关键热路径——每个中间件增加一次函数调用和闭包分配,紧密循环中开销不可忽视
八、参考资料
- Koa 中间件机制 - 异步洋葱模型的原始实现
- gRPC-Go 拦截器链 - 生产级 RPC 中间件组合
- Express 中间件 - Node.js HTTP 中间件的事实标准
- Redux applyMiddleware - 状态管理中的中间件模式
- ASP.NET Core Middleware - .NET 请求管道实现
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






