Koa 中间件怎么写?洋葱模型和常见中间件一次搞懂
Koa 中间件就是一个 async 函数,接收 ctx(请求上下文)和 next(调用下游中间件)两个参数。调用 await next() 把控制权交给下一个中间件,等下游全部执行完再返回——这种"先进后出"的执行流程叫洋葱模型。
自定义中间件的关键是理解 await next() 这一行:它前面是请求阶段的逻辑,后面是响应阶段的逻辑。忘了 await,后置代码会和下游并发执行,时序全乱。
javascriptasync function myMiddleware(ctx, next) { // 请求阶段:进入洋葱 console.log(`${ctx.method} ${ctx.url}`); await next(); // 交出控制权,等下游执行完再回来 // 响应阶段:出洋葱 console.log(`status: ${ctx.status}`); } app.use(myMiddleware);
中间件的注册顺序决定执行顺序——先 app.use 的先进入,但最后出来。实际项目中最常踩的坑:错误处理中间件没放在最前面,导致下游抛的异常它捕获不到。
追问
洋葱模型的执行顺序具体是怎样的?
两个中间件就能看清楚:
javascriptapp.use(async (ctx, next) => { console.log('A 进'); await next(); console.log('A 出'); }); app.use(async (ctx, next) => { console.log('B 进'); await next(); console.log('B 出'); }); // 输出:A 进 → B 进 → B 出 → A 出
底层实现是 koa-compose,它把中间件数组递归串联成一个 Promise 链。当你调用 await next() 时,实际调用的是 compose 生成的下一个函数;当所有中间件执行完,next 变成空函数,Promise 链开始回溯。
如何写可配置的中间件?
用工厂函数包一层,把配置项当参数传进去:
javascriptfunction createLogger(opts = {}) { return async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }; } app.use(createLogger({ format: 'tiny' }));
Koa 生态里的中间件(koa-bodyparser、koa-static 等)几乎都采用这个模式,好处是同一个中间件可以在不同路由上用不同配置。
常见中间件有哪些?各自解决什么问题?
| 类型 | 作用 | 代表实现 |
|---|---|---|
| 错误处理 | 兜底捕获异常,统一错误响应格式 | 自写,放最前面 |
| 日志 | 记录请求方法、路径、耗时 | koa-logger |
| 认证 | 校验 token,拒绝未授权请求 | koa-jwt |
| CORS | 设置跨域响应头 | @koa/cors |
| 请求体解析 | 把请求体解析到 ctx.request.body | koa-bodyparser |
| 静态文件 | 托管静态资源 | koa-static |
| 会话 | 管理 cookie/session | koa-session |
| 路由 | 路径匹配和参数提取 | @koa/router |
Koa 本身只封装了 ctx 和中间件机制,这些能力全部靠中间件补齐。和 Express 的区别是:Express 中间件是线性的,Koa 是洋葱式的,所以 Koa 的错误处理和响应修改更自然。
next() 不加 await 会怎样?
下游中间件照常执行,但你的后置逻辑会和下游并发运行,时序不可控。典型后果:日志中间件记录的 ctx.status 是默认的 404,因为真实状态码还没设置完你就读了;错误处理中间件捕获不到异常,因为错误还没抛出来你就已经退出了。只有一种场景可以不 await:发后即忘的埋点或异步通知,不阻塞响应。
多个中间件怎么组合管理?
用 koa-compose 把多个中间件合成一个,适合路由级别的中间件分组:
javascriptconst compose = require('koa-compose'); const authChain = compose([authMiddleware, rbacMiddleware]); router.get('/admin', authChain, adminHandler);
大项目里建议按目录组织:middleware/logger.js、middleware/auth.js,在 app.js 里按顺序引入。注册顺序就是执行顺序,搞错了很难排查,所以保持 app.use 列表简洁清晰很重要。