Express/Koa 中间件洋葱模型是什么?
简化版
中间件是请求处理链上的函数。Koa 的洋葱模型通过 await next() 实现前置逻辑和后置逻辑:请求进入时从外到内执行,响应返回时从内到外执行。Express 更偏顺序回调链。
详细版
Koa 示例:
app.use(async (ctx, next) => {
console.log('A before');
await next();
console.log('A after');
});
多个中间件执行顺序类似:
A before -> B before -> handler -> B after -> A after
适合做日志、鉴权、错误处理、响应时间统计、跨域、压缩等。
完整版教学
一、中间件是请求处理管线
一个 HTTP 请求进入应用后,通常要经过很多步骤:解析 body、处理 CORS、鉴权、路由、业务逻辑、错误处理、日志记录。中间件把这些横切逻辑拆成可组合函数。
这样业务 handler 不必关心所有通用细节。
二、Koa 洋葱模型的关键是 await next
await next() 前是进入下一层之前的逻辑,之后是下一层完成后的逻辑。这使得外层中间件可以包裹内层执行。
比如统计耗时:
app.use(async (ctx, next) => {
const start = Date.now();
await next();
ctx.set('X-Time', Date.now() - start);
});
三、Express 和 Koa 的差异
Express 中间件基于 req, res, next,更偏回调风格,生态成熟。Koa 更轻,基于 async/await,ctx 封装更清晰,洋葱模型更自然。
Express 也能写后置逻辑,但错误处理和异步传递方式与 Koa 不完全一样。
四、面试追问与工程落地
常见追问是“错误处理中间件放哪里”。通常错误处理要放在外层,包住后续中间件,这样内部 throw 的错误能被统一捕获并转成响应。
工程中中间件顺序非常重要。body parser 必须在需要读取 body 的路由前,鉴权应在受保护路由前,错误处理中间件要包住业务链路。顺序错了,功能就会诡异失效。
五、await next 决定控制权是否真正回来
Koa 的 next() 返回下游中间件完成的 Promise。外层只有 await next(),才能在下游响应或抛错之后恢复执行;漏掉 await 会让外层提前结束,耗时统计偏小,try/catch 也可能接不到后续 rejection。
A before
└─ await B before
└─ await handler
└─ B after
└─ A after
假设 B 和 handler 分别耗时 20 ms、30 ms,A 正确包裹时统计约 50 ms;若 A 只调用 next() 不等待,可能立即记录接近 0 ms。洋葱模型不是日志打印技巧,而是 Promise 调用栈的进入和回溯。
| 场景 | Koa | Express 5 |
|---|---|---|
| 普通中间件 | async (ctx, next) | (req, res, next) |
| 继续下游 | await next() | next() 或返回 Promise handler |
| 异步 throw/reject | 外层 try/catch 可捕获 | 返回的 rejected Promise 自动交给错误链 |
| 错误中间件位置 | 外层先注册以包住后续 | 四参数错误中间件通常放路由之后 |
六、顺序、终止条件和响应已发送边界
中间件既可以继续下游,也可以直接结束请求。鉴权失败时设置 401 后应 return,不能继续让业务 handler 写第二次响应;Express 中调用 next(err) 会跳过普通中间件进入错误链,错误处理中间件必须保持四参数签名。
Express 5 已能自动转发返回 Promise 的 rejection;Express 4 项目常需要包装 async handler 或显式 .catch(next)。无论框架版本,启动后台 Promise 却不 return/await,都游离在请求链外,统一错误中间件无法可靠接管。
当响应头已经发送后才发生流式错误,不能再改成一份完整 JSON 错误体;Express 自定义错误处理中间件应检查 headersSent 并交给默认处理关闭连接。Koa 也要区分响应尚未提交与 socket 已部分写出的情况。
中间件排查顺序:先看注册位置,再看 next 是否调用和等待,最后看响应是否已经结束;三者决定控制流,而不是函数名。
七、常见误区与追问
- 误区:调用 next() 就等于 await next()。 前者只启动下游,后者才等待下游完成并恢复后置逻辑。
- 误区:Express 完全没有洋葱式后置逻辑。 Promise/回调之后仍可写后置代码,但控制流和错误约定与 Koa 不同。
- 误区:错误中间件放在最前面才能捕获 Express 路由错误。 Express 错误链按注册顺序向后寻找四参数处理器,通常应放路由之后。
- 追问:Koa 错误处理中间件为什么先注册? 它要作为外层
try { await next() }包住后续整条链。 - 追问:Express 5 对 async handler 有什么变化? 返回 Promise 的 throw/rejection 会自动调用错误流程,Express 4 不能一概类推。
- 追问:为什么日志中间件适合放外层? 它可以覆盖解析、鉴权、路由和业务的总耗时及最终状态。
- 追问:中间件怎样主动终止链? 不调用 next,并明确设置响应;之后应 return,避免继续写响应或业务副作用。
八、加强记忆
中间件是“请求流水线”,Koa 洋葱模型是“进去一层层 before,出来一层层 after”。await next() 是分界线。Express 还要记住顺序决定匹配与错误传播,Express 5 只会自动转发处理器返回的 Promise rejection。