端点会写了,本节给服务端装"进站安检"与"参数面板":server/middleware 在每个请求到达端点前拦截,适合日志、CORS、鉴权预检;runtimeConfig 与环境变量让同一份产物在不同环境拿到不同配置。这两块与 5.2 的路由中间件、2.2 的配置分层遥相呼应,合起来构成完整的服务端工程面。
server/middleware 下的文件对每个请求执行(包括页面请求与所有 API),且不生成路由——纯粹的路过拦截。执行顺序按文件名字典序,需要顺序时用数字前缀:
// server/middleware/1.logger.ts // 请求日志:谁、何时、要什么、多久 export default defineEventHandler((event) => { const start = Date.now() event.node.res.on('finish', () => { console.log( `[api] ${event.method} ${event.path} ` + `${event.node.res.statusCode} ${Date.now() - start}ms` ) }) })
// server/middleware/2.auth-context.ts // 鉴权预检:解析会话,塞进请求上下文,端点直接读 export default defineEventHandler(async (event) => { const token = getHeader(event, 'authorization')?.replace('Bearer ', '') // 解析结果挂在 context 上,后面的端点与中间件都能用 event.context.user = token ? await verifySession(token) // 查会话存储,返回用户或 null : null })
// server/api/orders/index.get.ts // 端点消费上下文:中间件铺好的路直接走 export default defineEventHandler((event) => { if (!event.context.user) { throw createError({ statusCode: 401, statusMessage: '请先登录' }) } return db.orders.findMany({ where: { userId: event.context.user.id } }) })
TypeScript 项目给 context 补类型声明后,event.context.user 有完整的类型提示。这套"中间件解析、端点消费"的模式把鉴权从每个端点里抽走,端点只关心业务。
CORS 也是标准中间件场景——不过 Nitro 内置了配置化方案,优先用它:
// nuxt.config.ts export default defineNuxtConfig({ nitro: { routeRules: { '/api/public/**': { cors: true }, // 这组接口允许跨域 }, }, })
两类中间件名字像,职责隔着一层接力棒:
| 维度 | 路由中间件(5.2) | 服务端中间件(本节) |
|---|---|---|
| 位置 | middleware 目录 | server/middleware 目录 |
| 拦截对象 | 页面导航(首航 + 换页) | 一切请求(页面 + API + 静态) |
| 运行端 | 服务端首航、浏览器换页 | 只在服务端 |
| 典型用途 | 登录守卫、权限改道 | 日志、CORS、鉴权预检、安全头 |
| 能否挡页面 | 能(navigateTo 改道) | 能(直接返回响应短路) |
一句话分工:路由中间件管"用户能去哪个页面",服务端中间件管"请求能不能进来"。安全敏感的鉴权必须两层都做——前端守卫挡误入,服务端校验挡绕过(用户可以绕过页面导航,绕不过 HTTP 请求本身)。
2.2 讲过 runtimeConfig 的结构,这里落到服务端代码。三条纪律:
纪律一:密钥只在服务端读。
// server/api/admin/stats.get.ts export default defineEventHandler((event) => { const config = useRuntimeConfig(event) // server-only 区域:数据库密码、第三方私钥放这里 const stats = await fetchAdminStats(config.dbPassword) // public 区域的值服务端也能读(比如拼接口地址) const base = config.public.apiBase return { stats, base } })
纪律二:环境变量覆盖,产物不变。构建一次、多环境部署的核心机制:
# 预发环境启动 NUXT_DB_PASSWORD=staging-pass node .output/server/index.mjs # 生产环境启动(同一份产物) NUXT_DB_PASSWORD=prod-pass node .output/server/index.mjs
纪律三:别把环境差异编译进产物。import.meta.env.XXX 这类构建期内联的值,改一次要重新构建;runtimeConfig 是运行时读取,环境变量注入即生效。需要多环境切换的值一律走 runtimeConfig。
第 4 章警告过"进程级变量跨用户污染",正解在这里:Nitro 内置 storage 抽象,跨请求、跨部署形态的安全存取:
// server/api/featured.get.ts // 热门商品:五分钟缓存,过期重算 export default defineEventHandler(async () => { const storage = useStorage('cache') let featured = await storage.getItem('featured-products') if (!featured) { featured = await computeFeatured() // 昂贵的计算或查询 await storage.setItem('featured-products', featured, { ttl: 300 }) } return featured })
storage 后端可插拔:开发模式用内存,生产配 Redis 时多实例共享缓存,配置进 nitro.storage 项。它与 3.3 的 swr 缓存互补:swr 缓存的是整页 HTML,storage 缓存的是业务数据块——两层缓存搭配,服务器的重复劳动降到最低。
⚠️ 常见坑:服务端中间件里做重活拖慢全站。每个请求都过中间件,一次两百毫秒的远程校验等于全站延迟加两百毫秒。重逻辑挪进端点或用 storage 缓存结果,中间件只做轻量解析。
💡 关键直觉:把服务端中间件想成机场安检(人人过、快进快出),路由中间件想成登机口检票(只管本航班)——安检在前、检票在后,缺一不可但职责不混。