本节进入第一棒内部:一次请求从浏览器敲下回车到 HTML 返回,Nuxt 服务器里依次发生什么。它是第 4 章(水合)、第 5 章(路由中间件)、第 6 章(服务端中间件)的时序基准——后面所有"什么时候执行"的问题,都对照本节的管线图回答。

对着图里标号走一遍细节:
② 服务端中间件。Nitro 层的请求拦截(server/middleware 目录),在 Vue 应用创建之前运行,能改请求上下文、设置响应头,适合日志、鉴权预检、CORS 处理——细节第 6 章展开。注意它与"路由中间件"(④,middleware 目录)是两个物种:前者拦一切请求含 API,后者只拦页面导航。
③ 插件执行。plugins 目录的文件在应用创建后按文件名顺序执行,负责注入全局能力。插件拿到 nuxtApp 实例,是注册生命周期钩子的标准场所:
// plugins/trace.ts:登记钩子观察管线 export default defineNuxtPlugin((nuxtApp) => { nuxtApp.hook('app:created', () => { console.log('Vue 应用已创建') }) nuxtApp.hook('page:finish', () => { console.log('页面渲染完成') }) })
④⑤ 组件与数据。路由中间件跑完后,页面组件的 setup 在服务端执行;其中的 useAsyncData 发起取数,Nuxt 会等待所有 Promise 落定后再进入渲染——这是"首屏 HTML 里就有数据"的机制保证。
⑥⑦ 渲染与交付。组件树被序列化为 HTML 字符串;同时,异步数据被序列化成 payload,以脚本形式嵌入页面。浏览器水合时直接读 payload,不重复请求——第 4 章的交接物资在这里装车。
按阶段整理常用钩子:
| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
| app:created | Vue 应用实例创建后 | 初始化全局状态 |
| page:start | 页面组件开始渲染 | 开启加载指示 |
| page:finish | 页面渲染完成 | 关闭加载指示 |
| app:rendered | SSR 渲染完成(仅服务端) | 抓取渲染结果做审计 |
| render:response | 响应发送前(仅服务端) | 修改最终 HTML/头 |
| app:suspense:resolve | 水合 suspense 解除 | 客户端首屏后动作 |
| page:loading:end | 页面加载状态结束 | 隐藏全局进度条 |
钩子解决的是"想在框架流程里插一脚"的需求。比如给所有页面响应统一加安全响应头,render:response 是正解;而想统计服务端渲染耗时,app:rendered 里读时间戳即可。
同一份 setup 代码,服务端渲染时执行一次,客户端水合时再执行一次。写跨端代码的三板斧:
<script setup> // 判端三板斧:编译期常量、修饰符文件、生命周期 import { ClientOnly } from '#build/components' // 概念示意 if (import.meta.server) { // 仅服务端执行:安全地读服务端环境变量 } onMounted(() => { // 仅客户端执行:访问 window、初始化图表 }) // 想按端拆分组件:同名 .server.vue 与 .client.vue 成对出现 // Nuxt 自动在对应端加载对应实现 </script>
最常见的翻车是服务端执行到 window 或 document:SSR 管线里没有浏览器对象,直接抛错让整页 500。防御写法是把这类代码挪进 onMounted,或用 ClientOnly 组件包裹模板片段。
钩子清单背不住很正常,需求驱动地过一遍用法比背表格有效。三个高频需求:
需求一:给所有响应加安全头。在 render:response 里改最终响应:
// plugins/security-headers.ts(服务端插件) export default defineNuxtPlugin((nuxtApp) => { nuxtApp.hook('render:response', (response) => { response.headers = response.headers || {} response.headers['X-Frame-Options'] = 'DENY' response.headers['X-Content-Type-Options'] = 'nosniff' }) })
注意这处理的是页面响应;API 响应的头要在 Nitro 层加(6.2 的服务端中间件或 9.3 的路由规则),两层各管各的出口。
需求二:服务端渲染耗时打点。app:rendered 触发时请求已渲染完,配合请求开始时间算总耗时:
export default defineNuxtPlugin((nuxtApp) => { if (import.meta.server) { nuxtApp.hook('app:created', () => { // 上下文里记起点 nuxtApp.ssrContext.startTime = Date.now() }) nuxtApp.hook('app:rendered', () => { const cost = Date.now() - (nuxtApp.ssrContext.startTime || 0) console.log(`[ssr] 渲染耗时 ${cost}ms`) }) } })
这类打点数据积累起来,就是服务器容量的规划依据——P95 渲染耗时升高的那周,通常也是该加实例的那周。
需求三:页面切换的全局加载条。page:start 开、page:finish 关,三行代码还原一个经典 UI:
export default defineNuxtPlugin((nuxtApp) => { const loading = useState('page-loading', () => false) nuxtApp.hook('page:start', () => { loading.value = true }) nuxtApp.hook('page:finish', () => { loading.value = false }) })
三个需求的共同模式:钩子是管线上的标准插座,插座的形状(参数与时机)由框架定义,插什么由你决定。找到"我想在哪个步骤之后做事",钩子名就是那个步骤的名字。
⚠️ 常见坑:在 setup 顶层放定时器、事件监听却不清理。服务端执行时创建的 interval 会挂在渲染进程上泄漏,请求一多进程就肿。服务端只做"渲染需要的事",副作用一律留给客户端生命周期。
💡 关键直觉:把 SSR 管线记成"中间件拦路、插件装弹、setup 取数、渲染出货、payload 同车"五步,任何执行时机问题先对号入座再查文档。