01|CSR、SSR 与 SSG
第八章 / 01。目标:不再用“快/慢”给架构贴标签,而是追踪 HTML 在何时、用谁的数据生成。
三条时间线
| 模式 | HTML 何时生成 | 首次请求拿到什么 | 数据新鲜度由谁决定 |
|---|---|---|---|
| CSR | 浏览器运行 JS 后 | HTML 壳、脚本和资源 | 客户端请求与缓存策略 |
| SSR / universal | 每次请求或服务器缓存命中时 | 针对该 URL/请求生成的 HTML | 请求时数据、服务器缓存键与 TTL |
| SSG / prerender | 构建时 | 已生成文件/CDN 响应 | 重新构建、按需再生或客户端刷新 |
SSR 的 HTML 可更早包含正文,但客户端若需要交互,仍要下载代码并水合。SSG 减少请求时计算,却可能展示构建时旧数据。CSR 对登录后台很自然,但公开内容在 JS 执行前通常没有业务正文。
先建立独立的 SSR 实验环境
本章使用独立目录 nuxt-lab,不把 Nuxt 装进前面 Vite 项目。使用 Node 24.15+ 的24系列。先创建空目录,进入后写 package.json:
{
"name": "nuxt-lab",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxt dev --host 127.0.0.1",
"build": "nuxt build",
"preview": "nuxt preview --host 127.0.0.1"
},
"dependencies": { "nuxt": "4.5.2", "vue": "3.5.42", "vue-router": "4.6.4" }
}在目录中执行 npm install 并保留锁文件,以后用 npm ci 复现。本章版本用于固定实验,不代表其他小版本不能用。Nuxt 4 的客户端代码在 app/,服务端路由在根目录 server/:
nuxt-lab/
package.json
nuxt.config.ts
app/
app.vue
pages/index.vue
pages/blog/[slug].vue
pages/products/[id].vue
pages/admin/editor.vue
components/ 第七节加入 Comments.vue
server/api/products/[id].get.tsapp/app.vue 让当前 URL 对应的页面出现在应用里:
<template><NuxtPage /></template>app/pages/index.vue:
<template>
<main>
<h1>Nuxt 渲染实验</h1>
<NuxtLink to="/blog/start">文章</NuxtLink>
<NuxtLink to="/products/1">商品</NuxtLink>
<NuxtLink to="/admin/editor">编辑器</NuxtLink>
</main>
</template>app/pages/blog/[slug].vue:
<script setup lang="ts">
const route = useRoute()
</script>
<template><h1>文章:{{ route.params.slug }}</h1></template>app/pages/admin/editor.vue:
<script setup lang="ts">
const text = ref('')
</script>
<template><h1>编辑器</h1><input v-model="text" aria-label="草稿"></template>Nuxt 根据 pages 的文件名生成路由,方括号段成为 params;NuxtPage/NuxtLink 自动注册。ref/useRoute 等受支持 API 也自动导入,仅在 Nuxt 编译的文件里成立,不能复制到普通 Node 脚本后省略 import。
server/api/products/[id].get.ts:
export default defineEventHandler(event => {
const id = getRouterParam(event, 'id')
if (id !== '1') throw createError({ statusCode: 404, statusMessage: 'Product not found' })
return { id, title: '机制笔记', fetchedAt: new Date().toISOString() }
})这里的 .get.ts 匹配 GET,/api/products/1 的处理器只在服务器执行。defineEventHandler/getRouterParam/createError 是 Nuxt 服务端提供的入口,返回普通对象由服务器编码为 JSON。
app/pages/products/[id].vue:
<script setup lang="ts">
const route = useRoute()
const { data, error } = await useFetch(() => `/api/products/${route.params.id}`)
if (error.value) throw createError({ statusCode: 404, statusMessage: 'Product not found' })
</script>
<template><article v-if="data"><h1>{{ data.title }}</h1><p>{{ data.fetchedAt }}</p></article></template>页面顶层 await 在服务端等待数据;useFetch 把结果放进 payload,让浏览器第一次执行同一页面时复用。下一节观察水合,第三节再讨论 key 和后续请求。首次运行时先用下面配置;npm run dev 启动后打开终端显示的本机 URL,访问 /products/1 与 /api/products/1,分别看到 HTML 页面与 JSON。这两者不是同一个输出。
Nuxt 4 的路由级实验
Nuxt 默认采用 universal rendering。把下面配置写入一个 Nuxt 4 实验项目的 nuxt.config.ts:
export default defineNuxtConfig({
nitro: { prerender: { routes: ['/blog/start'] } },
routeRules: {
'/': { prerender: true },
'/blog/**': { prerender: true },
'/products/**': { swr: 3600 },
'/admin/**': { ssr: false }
}
})运行 npm run build,成功后 npm run preview;按终端地址访问。静态路径 /blog/start 由 nitro.prerender.routes 显式枚举,动态 pattern 本身不会枚举无限 slug。构建并预览后,用“查看页面源代码”或 curl 检查业务标题是否在响应 HTML 中,再关闭浏览器 JavaScript 检查哪些路径仍可读。记录的是响应证据,不能只看客户端 Elements 面板,因为 CSR 执行后那里也会有 DOM。
swr: 3600 的具体缓存位置和一致性依赖 Nitro 部署适配器;缓存用户相关响应时,必须先设计用户/语言/权限等缓存键。不能把包含私有数据的页面按 URL 公共缓存。
选择顺序
- 内容是否对匿名访问者稳定且可在构建时枚举?是则先考虑 SSG。
- 是否必须按请求读取权限、Cookie 或强新鲜数据?考虑 SSR,同时定义缓存边界。
- 是否主要是登录后的高度交互工具,首屏正文与爬取不重要?CSR 可能更简单。
- 同一站点可混用,不必选一个模式统治所有路由。
noScripts 适用于完全无客户端交互的预渲染内容;只要页面需要一个可交互组件,就要保留脚本,并用组件边界控制水合时机。
练习:给四条路由做决策记录
为 /docs/:slug、/stock/:symbol、/account、/admin/editor 选择 CSR、请求时 SSR、SSG 或带再验证缓存的混合模式。每条必须写:HTML 生成时机、数据来源、缓存键/失效条件、无 JS 体验、需要水合的交互,以及一条失败场景。不能只写“为了 SEO”。
参考答案:第 8 章第 01 题。