Vue 基础体系 · 第 20/70 篇。示例基于 Vue 3、Composition API、TypeScript 与现代 Vite 工具链;版本敏感能力会单独标注。
Vue SSR 与 Nuxt:水合、数据获取、缓存、SEO 和部署边界
1. 先划清边界:SSR、CSR、SSG 和 Nuxt
服务端渲染(Server-Side Rendering,SSR)是指:一次 HTTP 请求到达服务器后,服务器执行 Vue 应用,得到包含页面内容的 HTML,再把 HTML 返回给浏览器。浏览器先显示这份 HTML,随后加载 JavaScript,将现有 DOM 与 Vue 组件树连接起来,这个过程叫水合(Hydration)。
**客户端渲染(Client-Side Rendering,CSR)**则通常只返回一个空的 HTML 容器,例如:
<div id="app"></div>
<script type="module" src="/assets/app.js"></script>
页面内容需要等 JavaScript 下载、执行并请求数据后才出现。
**静态生成(Static Site Generation,SSG)**是在构建阶段执行页面渲染,把结果保存为静态 HTML。请求到来时由 CDN 或静态服务器直接返回,不需要为每次请求执行 Vue。
Nuxt 并不等于“只能 SSR”。它基于 Vue 和 Nitro 提供了多种交付模式:
| 模式 | 页面生成时间 | 请求时是否执行服务端代码 | 常见用途 |
|---|---|---|---|
| SSR | 每次请求 | 是 | 个性化页面、实时内容 |
| SSG | 构建时 | 否,通常只读静态文件 | 文档、营销页、博客 |
| SWR/缓存化 SSR | 首次或缓存失效时 | 视缓存状态而定 | 访问量较大的内容页 |
| CSR | 浏览器运行时 | 否 | 管理后台、强交互应用 |
| 混合渲染 | 按路由分别决定 | 按路由决定 | 大多数生产 Nuxt 应用 |
Nuxt 的价值不只是“让 Vue 在服务器运行”,还包括:
- 文件系统路由;
- 服务端 API 和 Nitro 运行时;
- SSR 数据获取与序列化;
- 页面元数据管理;
- 路由级缓存和渲染规则;
- Node、Serverless、Edge、静态托管等部署适配。
因此,选择 Nuxt 时必须同时考虑渲染方式、数据生命周期、缓存边界和部署运行时,而不是只看页面是否能输出 HTML。
2. SSR 的完整请求链路
一次典型的 Nuxt SSR 请求可以抽象为:
sequenceDiagram
participant B as 浏览器
participant S as Nuxt/Nitro 服务端
participant A as 后端 API 或数据库
participant C as 客户端 Vue 应用
B->>S: GET /articles/vue-ssr
S->>A: 获取文章数据
A-->>S: 返回文章数据
S->>S: 执行 Vue setup、渲染组件、生成 head
S-->>B: HTML + Nuxt payload + JS 资源地址
B->>B: 解析并显示 HTML
B->>S: 请求 JS、CSS 等静态资源
B->>C: 创建 Vue 应用并水合已有 DOM
C->>C: 使用序列化数据恢复响应式状态
C-->>B: 页面进入可交互状态
其中有三个容易混淆的结果:
- HTML负责让用户和爬虫尽早看到内容;
- payload负责把服务端已经获取的数据交给客户端,避免水合时无条件重复请求;
- JavaScript负责恢复事件监听、响应式更新和后续交互。
SSR 并不意味着浏览器永远不请求 API。它通常意味着:
- 首次请求由服务端获取数据并生成 HTML;
- 客户端水合时复用服务端数据;
- 后续路由切换、刷新或交互可能在浏览器中继续请求数据。
3. 水合到底做了什么
3.1 水合不是“重新渲染一遍页面”
Vue SSR 阶段会在服务器创建应用并执行组件树,生成 HTML:
<template>
<h1>{{ title }}</h1>
</template>
服务器可能生成:
<h1>Vue SSR</h1>
客户端启动时,Vue 会创建同样的组件树,并检查已有 DOM 是否符合预期。若符合,Vue 会复用这些节点并安装事件监听,而不是简单地删除所有内容再重新创建。
因此,水合要求服务器和客户端在初始状态下具有足够一致的渲染结果。
可以把要求形式化为:
其中:
- 是 Vue 的渲染过程;
- 是服务端初始状态;
- 是客户端水合前恢复的初始状态;
- 、 是服务端和客户端环境;
- 、 是最终用于比较的 DOM 结构。
理想条件是:
这里的 normalize 表示忽略浏览器可能自动补全的部分细节后进行结构比较。这个等式不要求服务端和客户端的所有运行环境相同,但要求首屏输出所依赖的状态和结构一致。
3.2 常见水合不一致来源
下面的代码在 SSR 中是不稳定的:
<script setup lang="ts">
const now = new Date().toLocaleString()
const random = Math.random()
</script>
<template>
<p>{{ now }}</p>
<p>{{ random }}</p>
</template>
服务端和客户端分别执行时:
new Date()可能跨越了时间边界;toLocaleString()可能使用不同的时区或语言环境;Math.random()几乎必然不同。
于是服务器输出:
<p>2025/01/01 10:00:00</p>
<p>0.123</p>
客户端预期却可能是:
<p>2025/01/01 18:00:01</p>
<p>0.845</p>
这就违反了水合一致性条件。
其他常见原因包括:
- 在模板或
setup()中直接访问window、document、localStorage; - 服务端和客户端使用不同的时区、语言或用户代理判断;
- 依赖对象属性枚举顺序或不稳定排序;
- SSR 时数据为空,客户端首次执行时数据已经改变;
- 服务端输出一个结构,客户端根据权限或浏览器能力输出另一个结构;
- 无效 HTML 导致浏览器在解析阶段自动修正 DOM;
- 组件随机生成 ID,但没有把 ID 作为服务端状态传给客户端。
3.3 正确处理浏览器专属逻辑
浏览器专属逻辑应推迟到客户端挂载后:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const width = ref<number | null>(null)
onMounted(() => {
width.value = window.innerWidth
})
</script>
<template>
<p v-if="width === null">正在读取窗口尺寸</p>
<p v-else>窗口宽度:{{ width }}</p>
</template>
服务端和客户端首次水合时都输出“正在读取窗口尺寸”,所以结构一致;挂载完成后,客户端再更新为真实宽度。
Nuxt 还提供了客户端专属组件:
<ClientOnly>
<BrowserChart />
<template #fallback>
<p>图表加载中</p>
</template>
</ClientOnly>
ClientOnly 的代价是:其内容不会作为正常 SSR HTML 输出。因此,不能把 SEO 关键标题、正文和主要链接全部放进 ClientOnly,否则爬虫和禁用 JavaScript 的访问者可能看不到它们。
3.4 “忽略水合警告”不是修复
某些框架版本支持对特定节点抑制水合警告,但这只是在隐藏症状,并不会让服务端和客户端状态一致。若节点中包含价格、权限、订单状态等业务数据,静默不一致可能产生更严重的问题:
- 用户看到的 HTML 与交互状态不同;
- 点击事件绑定到了错误的元素;
- 客户端重绘导致页面闪烁;
- 服务器展示了用户无权看到的缓存内容。
正确顺序应是先找出不一致来源,再决定是否把该区域改成客户端专属内容。
4. Nuxt 的数据获取:服务端首屏与客户端导航
4.1 为什么不应在 SSR 页面中直接写裸 $fetch
例如:
<script setup lang="ts">
const article = await $fetch('/api/article')
</script>
这段代码可能在服务端执行一次,并在客户端水合时再次执行一次。对于简单场景它可以工作,但它没有直接表达:
- 服务端结果如何交给客户端;
- 请求状态如何管理;
- 错误如何进入 Nuxt 页面状态;
- 路由参数变化时是否重新请求;
- 多个组件如何避免重复请求。
Nuxt 推荐使用 useFetch 或 useAsyncData。它们是 SSR 感知的数据获取组合式 API,能够把服务端结果放入 Nuxt 的 payload,使客户端水合时复用结果。
4.2 一个可运行的最小示例
先创建一个 Nuxt 项目:
npx nuxi@latest init nuxt-ssr-demo
cd nuxt-ssr-demo
npm install
npm run dev
下面创建一个服务端 API:
// server/api/articles/[slug].get.ts
import { createError, defineEventHandler, getRouterParam } from 'h3'
interface Article {
slug: string
title: string
description: string
content: string
}
const articles: Record<string, Article> = {
'vue-ssr': {
slug: 'vue-ssr',
title: 'Vue SSR 入门',
description: '理解服务端渲染和水合。',
content: '这是一篇示例文章。'
}
}
export default defineEventHandler((event) => {
const slug = getRouterParam(event, 'slug')
const article = slug ? articles[slug] : undefined
if (!article) {
throw createError({
statusCode: 404,
statusMessage: 'Article Not Found'
})
}
return article
})
再创建页面:
<!-- pages/articles/[slug].vue -->
<script setup lang="ts">
interface Article {
slug: string
title: string
description: string
content: string
}
const route = useRoute()
const {
data: article,
error,
status,
refresh
} = await useFetch<Article>(
() => `/api/articles/${encodeURIComponent(String(route.params.slug))}`,
{
server: true,
dedupe: 'cancel'
}
)
useSeoMeta(() => ({
title: article.value?.title ?? '文章不存在',
description: article.value?.description ?? '文章页面',
ogTitle: article.value?.title,
ogDescription: article.value?.description
}))
</script>
<template>
<main>
<p v-if="status === 'pending'">加载中……</p>
<div v-else-if="error">
<h1>文章加载失败</h1>
<p>{{ error.statusMessage || error.message }}</p>
<button type="button" @click="refresh()">重试</button>
</div>
<article v-else-if="article">
<h1>{{ article.title }}</h1>
<p>{{ article.description }}</p>
<div>{{ article.content }}</div>
</article>
</main>
</template>
访问:
http://localhost:3000/articles/vue-ssr
预期过程如下:
- 请求到达 Nuxt 服务端;
useFetch在 SSR 阶段调用/api/articles/vue-ssr;- API 返回文章;
- 服务端输出包含文章标题和正文的 HTML;
- 数据同时进入 Nuxt 的服务端到客户端传输状态;
- 浏览器加载 JavaScript 后,客户端复用该数据完成水合;
- 通常不会因为水合而再次发起同一个首屏请求;
- 请求不存在的 slug 时,API 返回 404,页面进入
error分支。
useFetch 的 URL 可以是响应式函数,因此路由参数变化时会根据依赖重新请求。dedupe: 'cancel' 表示相同数据键存在并发请求时,倾向于取消旧请求,避免旧响应覆盖新状态。具体去重行为受 Nuxt 版本和数据键影响,不能把它当作任意请求的全局竞态管理器。
4.3 useFetch 与 useAsyncData 的分工
useFetch 适合 HTTP 请求:
const { data, error } = await useFetch<User>('/api/me')
useAsyncData 更通用,适合组合多个来源:
const { data, error } = await useAsyncData('dashboard', async () => {
const [user, notifications] = await Promise.all([
$fetch('/api/me'),
$fetch('/api/notifications')
])
return { user, notifications }
})
这段代码的意义是:
useAsyncData负责 SSR 数据生命周期、状态和 payload;$fetch负责实际异步调用;Promise.all使两个独立请求并行,而不是串行等待;- 任意一个请求失败,整个组合结果失败。
useAsyncData 的 handler 应返回有效结果。某些 Nuxt 版本在 handler 返回 undefined 或 null 时,客户端可能认为服务端没有得到有效结果并再次请求。因此,空数据最好明确返回结构,例如:
return {
items: [],
total: 0
}
而不是隐式返回 undefined。
4.4 服务端数据和客户端数据必须隔离
不要在模块顶层保存会被请求修改的可变状态:
// 错误风险示例
let currentUser: User | null = null
export default defineEventHandler(async (event) => {
currentUser = await getUserFromRequest(event)
return currentUser
})
在长生命周期 Node 进程中,模块级变量可能被多个请求共享。请求 A 的用户数据可能残留给请求 B,形成严重的数据泄露。
应把请求相关状态放在请求上下文、组合式函数实例或明确的服务端存储中。例如,使用事件对象读取当前请求:
export default defineEventHandler((event) => {
const authorization = getHeader(event, 'authorization')
return {
authenticated: Boolean(authorization)
}
})
同理,不能把用户身份、购物车、权限判断结果放进全局单例缓存,除非缓存键明确包含租户、用户、权限版本等隔离维度。
5. 数据请求的并发、取消和错误恢复
SSR 和客户端导航的并发模型不同。
5.1 SSR 请求失败
在服务端渲染期间,如果关键数据请求失败,常见选择有两种:
- 让页面返回错误状态,例如 404 或 500;
- 保留页面壳体,显示降级内容。
文章详情这种资源通常应该区分“资源不存在”和“暂时失败”:
if (response.status === 404) {
throw createError({
statusCode: 404,
statusMessage: '文章不存在'
})
}
if (!response.ok) {
throw createError({
statusCode: 502,
statusMessage: '上游文章服务暂时不可用'
})
}
如果把所有失败都渲染成 HTTP 200 的“加载失败页面”,搜索引擎和监控系统会误判页面可用,缓存层也可能缓存错误 HTML。
5.2 客户端导航竞态
假设用户快速点击:
/articles/a
/articles/b
/articles/c
如果请求 A、B、C 同时发出,而响应顺序为:
C 先返回,B 后返回,A 最后返回
没有竞态控制时,A 的旧响应可能覆盖当前页面 C 的数据。
处理竞态至少需要满足:
常见手段包括:
- 使用稳定且包含参数的数据键;
- 使用
dedupe: 'cancel'或适合场景的defer; - 为每次请求创建
AbortController; - 在响应落地前检查请求序号;
- 在 Vue 版本支持时使用
onWatcherCleanup清理旧请求。
若直接使用 $fetch 编写自定义 watcher,需要显式传递取消信号:
import { ref, watch } from 'vue'
const slug = ref('vue-ssr')
const article = ref<Article | null>(null)
const loading = ref(false)
const error = ref<unknown>(null)
let requestId = 0
watch(slug, async (currentSlug) => {
const id = ++requestId
loading.value = true
error.value = null
try {
const result = await $fetch<Article>(
`/api/articles/${encodeURIComponent(currentSlug)}`
)
if (id === requestId) {
article.value = result
}
} catch (err) {
if (id === requestId) {
error.value = err
}
} finally {
if (id === requestId) {
loading.value = false
}
}
})
这里的 requestId 是逻辑取消:旧请求仍可能在网络层继续,但旧结果不会写入当前状态。生产代码还可以配合 AbortController 真正终止支持取消的请求。
6. Nuxt 中的缓存:缓存的不是同一个东西
“Nuxt 有缓存”不是一个完整命题。至少要区分以下层次:
浏览器 HTTP 缓存
↓
CDN / 反向代理缓存
↓
Nitro 路由或响应缓存
↓
Nuxt 数据缓存与 payload
↓
应用内部缓存
↓
数据库或上游 API 缓存
每一层的键、生命周期和失效方式都不同。
6.1 浏览器和 CDN 缓存
如果响应包含:
Cache-Control: public, max-age=60, s-maxage=600
通常可以理解为:
- 浏览器最多使用 60 秒的新鲜响应;
- 共享缓存(例如 CDN)最多使用 600 秒的新鲜响应。
但带有用户身份、Cookie、权限或购物车信息的 HTML 不应直接使用公共缓存:
Cache-Control: private, no-store
否则可能发生:
- 用户 A 的 SSR HTML 被 CDN 缓存;
- 用户 B 请求相同 URL;
- CDN 直接返回 A 的内容;
- B 看到错误的用户信息。
6.2 Nuxt/Nitro 的路由缓存
Nuxt 支持通过 routeRules 为不同路由设置渲染和缓存策略。一个示意配置如下:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/docs/**': { swr: 300 },
'/account/**': {
ssr: true,
headers: {
'cache-control': 'private, no-store'
}
},
'/api/**': {
headers: {
'cache-control': 'private, no-store'
}
}
}
})
这里的 swr: 300 表示允许缓存内容,并在一定时间后重新生成或更新。它适合公开、可容忍短暂陈旧的文章内容,不适合用户余额、权限和订单状态。
routeRules 的具体缓存实现取决于 Nitro 适配器和部署平台:
- Node 部署可能使用进程内或运行时缓存;
- Serverless 环境中的实例可能随时销毁;
- 多实例部署必须考虑缓存是否共享;
- Edge 平台可能有自己的区域缓存和失效机制。
因此,不能仅凭本地开发环境验证结果,就推断生产环境的缓存行为。
6.3 数据缓存与 HTML 缓存不是一回事
缓存整份 HTML:
URL -> 已渲染 HTML
缓存 API 数据:
API 参数 -> JSON 数据
二者的失效后果不同。若文章 HTML 缓存 300 秒,编辑发布后用户可能继续看到旧标题;若只缓存 API 数据,页面壳体可能实时生成,但数据仍可能陈旧。
还要注意缓存键必须包含所有影响输出的变量。例如页面结果依赖:
页面 URL + 语言 + 租户 + 用户权限
却只用 URL 作为缓存键,就会造成跨语言、跨租户或跨权限污染。
6.4 Nuxt payload 不是长期业务缓存
Nuxt 会将 SSR 阶段的数据传给客户端,客户端水合时复用。这种数据传输的主要目的,是避免首屏数据重复获取,而不是替代 Redis、CDN 或数据库缓存。
不要把以下内容放进会序列化到客户端的状态:
- 数据库密码;
- 服务端 API Token;
- 内部网络地址;
- 不应暴露给用户的权限信息;
- 其他用户的隐私数据。
服务端返回给浏览器的任何 payload 都应视为用户可读取的数据。若某个数据只服务于服务器渲染,就应在服务端完成处理,只把必要的公开结果发送给客户端。
7. SSR 为什么有利于 SEO,但不等于自动做好 SEO
**SEO(Search Engine Optimization,搜索引擎优化)**关注的是搜索引擎能否发现、抓取、理解并正确展示页面。
SSR 的直接优势是:首个 HTTP 响应中已经包含正文、标题和链接。例如:
<title>Vue SSR 入门</title>
<meta name="description" content="理解服务端渲染和水合。">
<h1>Vue SSR 入门</h1>
<p>这是一篇示例文章。</p>
这比“HTML 只有一个空容器,正文等 JavaScript 执行后才出现”更容易被抓取,也更有利于首屏可见内容。
但 SSR 不自动解决以下问题:
- 页面没有唯一、准确的
<title>; - 多个 URL 返回相同内容;
- canonical 地址错误;
- 关键正文被
ClientOnly包裹; - 服务器返回 200,但正文实际是“资源不存在”;
- 没有站点地图和可抓取链接;
- robots 配置阻止了抓取;
- 页面加载很慢,首屏数据请求经常超时。
Nuxt 页面可以使用 useSeoMeta:
useSeoMeta(() => ({
title: article.value?.title ?? '文章',
description: article.value?.description ?? '',
ogTitle: article.value?.title ?? '文章',
ogDescription: article.value?.description ?? '',
ogType: 'article'
}))
如果页面数据来自 SSR,服务器生成的 HTML 中应包含对应的 head 标签。若数据只在 onMounted 中获取,服务器无法生成准确标题和描述。
7.1 SEO 数据失败时要返回正确状态
以文章详情为例:
- slug 不存在:返回 404;
- 数据库暂时不可用:返回 5xx 或可监控的降级状态;
- 有权限但未登录:返回 401/403 或重定向;
- 页面公开且数据为空:才考虑正常返回空状态。
错误页面的 HTTP 状态和 HTML 内容必须一致。否则会产生软 404:用户看到“文章不存在”,但服务器却返回 200。
7.2 结构化数据和 SEO 的关系
结构化数据,例如 JSON-LD,可以帮助搜索引擎理解文章、产品或面包屑:
useHead(() => ({
script: article.value
? [
{
type: 'application/ld+json',
children: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.value.title,
description: article.value.description
})
}
]
: []
}))
这里的 JSON.stringify 内容必须经过可信数据处理。若把未经处理的用户输入直接拼入脚本字符串,应注意序列化和脚本注入风险。
8. SSR 的部署边界
8.1 Node SSR 部署
构建:
npm run build
Nuxt 会生成 .output 目录。Node 运行时通常这样启动:
node .output/server/index.mjs
启动前需要准备:
export NITRO_PORT=3000
export NITRO_HOST=0.0.0.0
node .output/server/index.mjs
预期结果是进程监听 0.0.0.0:3000,反向代理或负载均衡器再把公网请求转发到它。
部署验证至少包括:
curl -i http://127.0.0.1:3000/articles/vue-ssr
需要检查:
- 状态码是否为 200;
- HTML 是否已经包含文章标题;
- 是否返回正确的
Content-Type; - 资源 URL 是否能正常访问;
- 不存在的文章是否返回 404;
- API 失败时是否有可监控的 5xx。
Node SSR 的优势是能在请求时访问数据库、读取 Cookie、执行权限判断。代价是需要维护长期运行的服务,并处理:
- 进程重启;
- 多实例扩容;
- 内存泄漏;
- 连接池;
- 日志和健康检查;
- 多实例之间的缓存一致性。
8.2 静态生成部署
如果页面内容只在构建阶段确定,可以使用静态生成:
npx nuxt generate
生成后通常得到可由静态服务器托管的产物。此时部署到对象存储或 CDN 即可,不需要运行 Node SSR 进程。
静态生成的边界是:
- 构建后新增的动态 slug 不会自动出现;
- 依赖请求 Cookie 的页面无法在构建阶段生成个性化结果;
- 实时库存、订单状态、用户通知不适合直接静态化;
- 内容更新通常需要重新构建和发布,除非额外接入运行时数据请求。
静态化不是“SSR 更快的版本”,而是把渲染时机从请求阶段提前到了构建阶段。
8.3 Serverless 和 Edge
Nitro 可以将应用适配到不同运行时,但适配并不代表所有 Node 能力都存在。
Serverless 常见边界:
- 实例可能冷启动;
- 本地文件写入不可靠;
- 进程内缓存不稳定;
- 长连接和长任务可能受平台限制;
- 每次请求的执行时间可能有上限。
Edge 常见边界:
- 运行时可能不是完整 Node.js;
- 某些 Node 内置模块不可用;
- 数据库连接方式需要适配;
- 请求会在离用户较近的区域执行,但数据源未必也在附近;
- 全局变量更不能被视为可靠的持久缓存。
因此,代码应依赖部署目标支持的运行时 API,而不是默认服务器一定具备完整 Node 环境。
9. 渐进发布、缓存和回滚的相互影响
前端发布不仅是替换 JavaScript 文件,还涉及 HTML、payload、API 和 CDN 缓存之间的兼容性。
一次发布可能产生:
HTML 版本 A
引用的 JS 版本 B
payload 结构版本 A
API 响应版本 B
如果旧 HTML 被 CDN 缓存,而旧 HTML 引用的资源已经被删除,用户会得到 404;如果新客户端代码读取旧 payload 中不存在的字段,也可能在水合或导航阶段失败。
更稳妥的发布策略是:
- 静态资源文件使用内容哈希,例如
app.8f31.js; - 新资源先上传,再切换 HTML;
- 保留旧版本资源一段时间;
- API 做向后兼容,先支持旧客户端;
- HTML 和页面缓存设置合理的短 TTL;
- 回滚时同时恢复应用版本和必要的路由配置;
- 通过真实 URL 检查 SSR HTML、资源和 API 状态。
灰度发布还要考虑缓存命中。若 CDN 在灰度判断前就返回了缓存 HTML,那么用户未必真的访问了目标版本。需要明确:
- 灰度依据是 Cookie、Header、用户 ID 还是实例;
- CDN 是否会把灰度维度纳入缓存键;
- 是否允许不同版本共享静态资源;
- 回滚后旧客户端还能否访问旧 API。
10. 常见误解与诊断路径
误解一:SSR 页面就不会有白屏
SSR 只能保证服务器成功生成 HTML 时,浏览器有机会先显示内容。如果:
- SSR 等待上游 API 超时;
- 服务端 JavaScript 运行异常;
- CDN 缓存了错误响应;
- 页面关键内容被
ClientOnly包裹;
用户仍可能看到空白页、错误页或长时间等待。
诊断时先执行:
curl -sS https://example.com/articles/vue-ssr | grep -E '<h1>|<title>'
若响应 HTML 中没有正文,问题发生在 SSR、数据获取或缓存层,不是水合阶段。
误解二:水合警告只是开发环境噪声
水合警告说明服务端和客户端对同一节点的初始判断不同。应在浏览器开发者工具、服务端日志和实际 HTML 之间对比:
- 查看服务器返回的 HTML;
- 查看客户端水合前的状态;
- 搜索随机数、当前时间、时区、浏览器 API;
- 检查异步数据是否在服务端和客户端使用了同一个键;
- 检查条件渲染是否依赖只存在于浏览器的值。
误解三:useFetch 会自动缓存所有请求
useFetch 主要解决 SSR 数据传递、响应式状态和请求去重,不等于跨请求、跨实例、跨部署区域的业务缓存。
要实现跨请求缓存,必须明确使用:
- HTTP
Cache-Control; - CDN;
- Nitro 路由规则;
- Redis 或其他共享缓存;
- 上游 API 的缓存机制。
每种缓存都要明确缓存键、TTL、失效方式和隐私边界。
误解四:把所有页面改成 SSR 就能提升 SEO
如果页面没有可抓取的链接、正确的状态码、稳定的 canonical、准确的标题和正文,SSR 只是把问题更早地生成在服务器上。
SEO 的有效条件是:
SSR 主要改善“内容正确地出现在初始 HTML 中”这一部分,不能替代站点结构和内容策略。
11. 选择渲染策略的判断方法
可以按数据和交互边界做判断:
适合 SSG
- 内容在构建时已知;
- 更新可以接受重新构建;
- 页面面向所有用户相同;
- 希望最大化 CDN 静态分发。
例如文档、版本化教程、公司介绍页。
适合 SSR
- 页面必须根据请求实时生成;
- 需要读取 Cookie、地域或权限;
- 首屏内容依赖实时或准实时数据;
- 页面需要为搜索引擎提供动态正文。
例如公开文章详情、商品详情、区域化内容页。
适合 CSR
- 页面主要是登录后的管理界面;
- SEO 不重要;
- 内容高度依赖用户交互;
- SSR 会增加复杂度,却不能带来实际收益。
例如内部报表、拖拽编辑器和复杂工作台。
适合混合渲染
实际应用通常同时包含:
/ -> 静态生成
/articles/** -> SSR 或 SWR
/search -> SSR + 查询参数
/account/** -> SSR/CSR,禁止公共缓存
/admin/** -> CSR
/api/** -> 按接口权限和数据新鲜度配置
关键不在于选一个全局模式,而在于让每条路由的数据敏感性、SEO 要求、缓存策略和部署成本相互匹配。
当以下条件同时成立时,Nuxt SSR 才真正适合某个页面:
缺少任意一项,都可能出现相应问题:
- 不需要首屏 HTML:SSR 增加了服务器成本;
- 服务端无法访问数据源:SSR 只能返回加载壳体或失败;
- 无法保证一致:出现水合警告和状态覆盖;
- 无法定义缓存边界:出现陈旧数据或隐私泄露。
Vue SSR 与 Nuxt 的核心并不是把所有页面都搬到服务器,而是明确每一段代码在哪个运行时执行、数据在哪个阶段获取、结果在哪一层缓存,以及服务器生成的 DOM 如何安全地交给客户端继续运行。
系列导航与关联阅读
- 系列入口:Vue 完整学习路线:从响应式与组件到工程化、SSR 和生产交付
- 上一篇:Vue 组件库与设计系统:Token、主题、无障碍、文档和版本治理
- 下一篇:Vue 国际化工程:消息目录、Locale、日期数字、路由和回退
- 延伸:Vue 异步数据与请求状态:取消、竞态、缓存、分页和错误恢复
- 延伸:Vue 生产交付:环境配置、静态资源、缓存、灰度和回滚
官方资料
本文依据 Vue、Vite 与生态项目官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论