5月28日 04:26

Koa 中间件怎么写?洋葱模型和常见中间件一次搞懂

Koa 中间件就是一个 async 函数,接收 ctx(请求上下文)和 next(调用下游中间件)两个参数。调用 await next() 把控制权交给下一个中间件,等下游全部执行完再返回——这种"先进后出"的执行流程叫洋葱模型。

自定义中间件的关键是理解 await next() 这一行:它前面是请求阶段的逻辑,后面是响应阶段的逻辑。忘了 await,后置代码会和下游并发执行,时序全乱。

javascript
async function myMiddleware(ctx, next) { // 请求阶段:进入洋葱 console.log(`${ctx.method} ${ctx.url}`); await next(); // 交出控制权,等下游执行完再回来 // 响应阶段:出洋葱 console.log(`status: ${ctx.status}`); } app.use(myMiddleware);

中间件的注册顺序决定执行顺序——先 app.use 的先进入,但最后出来。实际项目中最常踩的坑:错误处理中间件没放在最前面,导致下游抛的异常它捕获不到。

追问

洋葱模型的执行顺序具体是怎样的?

两个中间件就能看清楚:

javascript
app.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 链开始回溯。

如何写可配置的中间件?

用工厂函数包一层,把配置项当参数传进去:

javascript
function 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.bodykoa-bodyparser
静态文件托管静态资源koa-static
会话管理 cookie/sessionkoa-session
路由路径匹配和参数提取@koa/router

Koa 本身只封装了 ctx 和中间件机制,这些能力全部靠中间件补齐。和 Express 的区别是:Express 中间件是线性的,Koa 是洋葱式的,所以 Koa 的错误处理和响应修改更自然。

next() 不加 await 会怎样?

下游中间件照常执行,但你的后置逻辑会和下游并发运行,时序不可控。典型后果:日志中间件记录的 ctx.status 是默认的 404,因为真实状态码还没设置完你就读了;错误处理中间件捕获不到异常,因为错误还没抛出来你就已经退出了。只有一种场景可以不 await:发后即忘的埋点或异步通知,不阻塞响应。

多个中间件怎么组合管理?

koa-compose 把多个中间件合成一个,适合路由级别的中间件分组:

javascript
const compose = require('koa-compose'); const authChain = compose([authMiddleware, rbacMiddleware]); router.get('/admin', authChain, adminHandler);

大项目里建议按目录组织:middleware/logger.jsmiddleware/auth.js,在 app.js 里按顺序引入。注册顺序就是执行顺序,搞错了很难排查,所以保持 app.use 列表简洁清晰很重要。

标签:Koa