Skip to content

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:

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/:

text
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.ts

app/app.vue 让当前 URL 对应的页面出现在应用里:

vue
<template><NuxtPage /></template>

app/pages/index.vue:

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:

vue
<script setup lang="ts">
const route = useRoute()
</script>
<template><h1>文章:{{ route.params.slug }}</h1></template>

app/pages/admin/editor.vue:

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:

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:

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:

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 公共缓存。

选择顺序 ​

  1. 内容是否对匿名访问者稳定且可在构建时枚举?是则先考虑 SSG。
  2. 是否必须按请求读取权限、Cookie 或强新鲜数据?考虑 SSR,同时定义缓存边界。
  3. 是否主要是登录后的高度交互工具,首屏正文与爬取不重要?CSR 可能更简单。
  4. 同一站点可混用,不必选一个模式统治所有路由。

noScripts 适用于完全无客户端交互的预渲染内容;只要页面需要一个可交互组件,就要保留脚本,并用组件边界控制水合时机。

练习:给四条路由做决策记录 ​

为 /docs/:slug、/stock/:symbol、/account、/admin/editor 选择 CSR、请求时 SSR、SSG 或带再验证缓存的混合模式。每条必须写:HTML 生成时机、数据来源、缓存键/失效条件、无 JS 体验、需要水合的交互,以及一条失败场景。不能只写“为了 SEO”。

参考答案:第 8 章第 01 题。

用心记录每一步成长