服务端中间件
本教程共 50 篇 · 第 34 篇 · 更新于 2026-08-08 · 约 7 分钟阅读
本节目标:能在
server/middleware/下写出服务端中间件,用来记录日志、统一加响应头或往请求上下文注入数据,并知道它不该做什么。
上两章我们写了 API 接口(处理具体请求)和 Nitro 引擎(支撑运行)。但有些逻辑不想绑在某个接口上,而是想「每个请求来了都先过一遍」——比如记一条访问日志、检查有没有登录态、给所有响应统一加个头。这种「请求守门员」就是服务端中间件。
34-1
Nuxt 会自动读取 server/middleware/ 目录下的每个文件,把它们注册成服务端中间件。每当有请求进来(无论最后是落到哪个 API 还是页面),中间件都会先于具体路由处理器执行。
它最适合做三件事:
- 记录日志:把每个请求的 URL、时间打出来,方便排查。
- 检查或补充:校验请求头、读取 cookie,把用户信息挂到上下文上供后续使用。
- 改写请求/响应:统一加响应头(如 CORS、安全头)。
34-2
中间件文件和接口一样,也是导出一个 defineEventHandler。最简单的日志中间件:
export default defineEventHandler((event) => {
console.log('新请求来了:' + getRequestURL(event))
})
每次收到请求,服务端控制台就会打印一行 URL。热更新照常生效,不用重启。
34-3
中间件最常干的活,是「提前准备好东西,让后面的接口直接用」。h3 的 event 上有一个 context 对象,专门干这个。比如我们在这里做个假的登录校验,把用户 id 挂上去:
export default defineEventHandler((event) => {
// 真实场景里会解析 cookie / token,这里简化演示
event.context.auth = { userId: 123 }
})
之后在任意接口里,都能直接读这个 auth:
export default defineEventHandler((event) => {
const auth = event.context.auth
return { userId: auth?.userId }
})
这样认证逻辑只写一遍,全站接口共享,不用每个文件重复解析。
34-4
中间件也能给响应加头,比如开放跨域(CORS)或一个安全相关的头:
export default defineEventHandler((event) => {
setResponseHeader(event, 'X-Powered-By', 'Nuxt')
})
Tip
setResponseHeader也是 h3 的助手。想批量加安全头(CSP、X-Frame-Options 等),中间件是统一入口,比在每个接口里写干净得多。
34-5
这是服务端中间件最容易写错的地方。中间件的设计目的是「看一眼、补一点、然后放行」,它不应该自己返回内容、结束响应或做重定向。换句话说:
- 别
return一个值(那会被当成响应体,导致真正的路由永远到不了)。 - 别调用
event.node.res.end()去结束请求。 - 别用
sendRedirect把请求拐走(除非你非常确定要拦截所有请求)。
正确姿势是:要么什么都不返回,只改 event;要么在条件不满足时 throw createError(...) 中断请求(比如鉴权失败返回 401)。
export default defineEventHandler((event) => {
const token = getHeader(event, 'authorization')
if (!token) {
// 鉴权失败:抛错误中断,而不是返回内容
throw createError({
statusCode: 401,
statusMessage: '未登录',
})
}
// 通过:把用户信息挂上上下文,放行
event.context.user = { token }
})
Warning中间件一旦
return了数据,那个请求就被它「截胡」了,后面的 API/页面处理器不会再执行。如果你的接口突然收不到请求,先检查中间件是不是不小心返回了东西。
34-6
新手常把两个名字相近的东西搞混,这里划清界限:
- 服务端中间件(
server/middleware/,本章):跑在服务器上,拦截「每个 HTTP 请求」,用来做日志、鉴权、加头。它操作的是 h3 的event。 - 路由中间件(
app/middleware/,第 19 章讲过):跑在「页面导航」层面,拦截「前端路由跳转」,用来做页面级权限控制(比如未登录跳登录页)。它操作的是 Nuxt 的路由上下文。
两者职责不同、所在目录不同、运行环境也不同。需要「每个网络请求都过一遍」用服务端中间件;需要「用户在前端切页面时拦一下」用路由中间件。
34-7
server/middleware/ 下可以有多个文件,它们会按文件名顺序依次执行。如果一个中间件往 event.context 注入了数据,后面的中间件和最终路由都能读到。利用这个顺序,你可以把「通用准备」放在靠前文件,「业务相关检查」放在靠后文件,逻辑更清晰。
Note中间件对所有请求生效,包括静态资源和页面请求。所以里面的逻辑要尽量轻量,别做太重的事,否则会拖慢每一个请求。
34-8
把前面几节压成一张清单,写中间件前扫一眼:
能做的:
- 读请求信息:URL、请求头、cookie(
getRequestURL、getHeader、getCookie)。 - 往
event.context挂数据,供后续接口读取。 - 用
setResponseHeader统一加响应头。 - 条件不满足时
throw createError中断请求(如 401)。
不能做的:
- 返回响应体:
return一个值会把请求「截胡」,真正的路由收不到。 - 调用
event.node.res.end()提前结束响应。 - 随意
sendRedirect把请求拐走,除非你确实想拦下所有请求。 - 做太重的事(查库、调外部接口):中间件对每个请求生效,会拖慢全站。
34-9
除了前面举例的,服务端中间件还适合这几类活。
记访问日志,把方法、URL、耗时打出来,是排查问题的第一步:
export default defineEventHandler((event) => {
const start = Date.now()
event.node.res.on('finish', () => {
console.log(getMethod(event), getRequestURL(event), Date.now() - start + 'ms')
})
})
改写请求上下文,比如根据子域名决定租户,挂到 event.context.tenant,后面所有接口直接读:
export default defineEventHandler((event) => {
const host = getRequestHost(event)
event.context.tenant = host.split('.')[0]
})
鉴权「预告」。中间件只做轻量校验和身份识别,把用户信息挂上 event.context,真正的权限判断留给各接口——这叫鉴权预告。重活(比如查库验证 token 合法性)如果很重,更适合放在具体接口或专门的工具函数里,别全压在中间件。
Note中间件对静态资源、页面 HTML 请求同样生效。给响应加头、记日志这类轻操作没问题;但别在中间件里调
readBody,它不仅对所有请求生效,还会和消费 body 的接口冲突。
34-10
服务端中间件是放在 server/middleware/ 下的「请求守门员」:每个请求都会先经过它,适合记日志、统一加响应头、往 event.context 注入共享数据。记住铁律——中间件只检查或补充,不要返回内容、不要结束响应(鉴权失败用 throw createError)。它和前端的「路由中间件」是两码事,别混。下一篇我们看怎么把密钥、接口地址这类配置安全地交给应用——runtimeConfig 与环境变量。
34-11 中间件的执行顺序与调试
当项目里有多个服务端中间件时,它们的执行顺序按文件名的字母排列。如果你需要控制顺序,可以在文件名前加数字前缀,如 01.cors.ts、02.auth.ts。数字小的先执行。这种机制在需要按特定顺序处理请求时非常重要,比如先处理跨域再验证身份。
调试中间件的一个实用技巧是在关键位置加 console.log,打印请求的方法、路径和当前中间件的名称。这能帮你确认中间件是否被触发、执行顺序是否正确。在服务端日志里,这些打印内容会出现在终端的输出中。
要注意的一个陷阱是:中间件的修改对同一个请求的后续中间件和路由处理函数立即可见。比如你在第一个中间件里往 event.context 挂了用户信息,后面的中间件和路由都能直接读到。这种共享机制很方便,但也意味着如果某个中间件忘了做它该做的事,后面的代码可能会因为缺少预期数据而出错。排查这类问题时,逐个检查中间件的执行链路是最有效的方法。