mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1290 字
3 分钟
中间件/管道链(Middleware / Pipeline Chain)
2026-06-13

一、为什么需要中间件/管道链#

你写了一个 HTTP 服务,最初只有业务逻辑。后来要加日志,就在处理函数开头写几行;要加认证,又在开头加一段校验;要加限流,再加一段计数逻辑。三个月后,业务逻辑被埋在几十行横切代码中间,改一个地方要翻半天。

更麻烦的是顺序问题。如果限流在认证之前运行,未认证的请求也会消耗限流配额——攻击者发一堆无效请求就能耗尽合法用户的配额。这种 bug 不是逻辑错误,而是代码组织方式导致的隐含依赖。

中间件/管道链把每个关注点抽成独立的中间件,按顺序串成链条。每个中间件只做一件事,然后调用 next() 把控制权交给下一个。不调用 next() 就短路整条链。请求向内流入,响应向外流出,形成「洋葱模型」。

二、现实类比#

机场安检通道。你的行李先过 X 光机(日志记录),然后过金属探测器(身份认证),再查验证件(参数校验)。每个关卡只做一件事,然后送你到下一个。任何一个关卡都可以拒绝你通过——相当于不调用 next() 短路整条链。

三、核心思想#

每个中间件接收一个上下文和一个 next() 函数。调用 next() 将控制传递给链中下一个中间件;next() 返回后,中间件可以运行后处理逻辑。不调用 next() 则短路整个链。这创建了一个双向管道——请求向内流入,响应向外流出。

flowchart TD A["中间件 A(日志)"] --> B["中间件 B(认证)"] B --> C["中间件 C(处理器)"] C --> B2["B 后处理"] B2 --> A2["A 后处理"]

执行顺序: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-Goserver.go#L1224-L1260 中的 chainUnaryServerInterceptors 将拦截器链接为单一处理器,getChainUnaryHandler 递归构建链,用于生产 gRPC 服务中的认证、日志、追踪和限流。
  • Koa.jsapplication.js#L152-L204use() 将中间件推入数组,callback() 通过 koa-compose 组合为单一函数。Koa 开创了异步洋葱模型,每个 await next() 创建一个栈帧,使下游中间件可以使用 try/catch/finally
  • Express.jsapp.use() 链接中间件用于 HTTP 请求处理,是 Node.js 生态最广泛使用的中间件实现。

七、小结#

何时使用:

  • HTTP 请求处理——认证、日志、CORS、压缩、限流作为可组合层
  • RPC 拦截器——gRPC 拦截器用于追踪、认证、重试和指标
  • 构建/编译管道——Webpack loader、Babel 转换各自处理后传递给下一个
  • CLI 命令处理——参数解析、验证、帮助生成作为中间件

何时不用:

  • 事件扇出(一对多)——需要多个独立处理器响应同一事件时,用观察者模式
  • 无状态转换——每步只是转换数据不需要包裹下一步时,用 array.map().filter().reduce()
  • 性能关键热路径——每个中间件增加一次函数调用和闭包分配,紧密循环中开销不可忽视

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

中间件/管道链(Middleware / Pipeline Chain)
https://blog.souloss.com/posts/programming/system-patterns/system-patterns-middleware-chain/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时