文章目录
- 前言
- 一、三种渲染模式
-
- [1.1 对比](#1.1 对比)
- [1.2 CSR 流程](#1.2 CSR 流程)
- [1.3 SSR 流程](#1.3 SSR 流程)
- [1.4 SSG 流程](#1.4 SSG 流程)
- [二、SSR 基本架构](#二、SSR 基本架构)
-
- [2.1 同构应用](#2.1 同构应用)
- [2.2 服务端渲染示例](#2.2 服务端渲染示例)
- [2.3 返回给浏览器的 HTML 结构](#2.3 返回给浏览器的 HTML 结构)
- [三、Hydration 客户端激活](#三、Hydration 客户端激活)
-
- [3.1 是什么](#3.1 是什么)
- [3.2 Hydration 不匹配](#3.2 Hydration 不匹配)
- [3.3 避免不匹配](#3.3 避免不匹配)
- 四、数据预取与状态同步
-
- [4.1 问题](#4.1 问题)
- [4.2 脱水与注水](#4.2 脱水与注水)
- [五、Streaming SSR(Vue 3)](#五、Streaming SSR(Vue 3))
-
- [5.1 传统 SSR 的问题](#5.1 传统 SSR 的问题)
- [5.2 流式渲染](#5.2 流式渲染)
- [六、SSR 开发注意事项](#六、SSR 开发注意事项)
-
- [6.1 生命周期](#6.1 生命周期)
- [6.2 浏览器 API](#6.2 浏览器 API)
- [6.3 第三方库](#6.3 第三方库)
- [6.4 Teleport 与 SSR](#6.4 Teleport 与 SSR)
- [七、Nuxt 简介](#七、Nuxt 简介)
-
- [7.1 为什么用 Nuxt](#7.1 为什么用 Nuxt)
- [7.2 Nuxt 3 vs Nuxt 2](#7.2 Nuxt 3 vs Nuxt 2)
- [八、何时选 SSR](#八、何时选 SSR)
- 九、面试聚焦
-
- [9.1 onMounted 只在客户端执行](#9.1 onMounted 只在客户端执行)
- [9.2 Hydration 不匹配会怎样?](#9.2 Hydration 不匹配会怎样?)
- [9.3 SSR 中能用 window/document 吗?](#9.3 SSR 中能用 window/document 吗?)
- [9.4 SSR 和 SSG 区别?](#9.4 SSR 和 SSG 区别?)
- 十、易混淆点
- 十一、思考与练习
- 总结
前言
SSR(Server-Side Rendering)在服务端将 Vue 组件渲染成 HTML 再发给浏览器,可提升首屏速度与 SEO。本篇会讲清楚:
- SSR、CSR、SSG 的区别
- 渲染流程与 Hydration 激活
- 数据预取与状态脱水/注水
- SSR 开发注意事项与 Nuxt 简介
一、三种渲染模式
1.1 对比
| 模式 | 首屏 HTML | 渲染位置 | SEO | 适用 |
|---|---|---|---|---|
| CSR | 空壳 + JS | 浏览器 | 差 | 中后台、强交互 |
| SSR | 完整 HTML | 服务器每次请求 | 好 | 内容站、电商详情 |
| SSG | 完整 HTML | 构建时预渲染 | 好 | 博客、文档、营销页 |
1.2 CSR 流程
浏览器请求 → 返回 index.html(几乎为空)
↓
下载 JS → 执行 Vue → 请求 API → 渲染页面
首屏白屏时间长,爬虫难以抓取动态内容。
1.3 SSR 流程
浏览器请求 → 服务器执行 Vue → 生成 HTML + 嵌入状态
↓
返回完整 HTML(用户立刻看到内容)
↓
下载 JS → Hydration 激活 → 变为可交互 SPA
1.4 SSG 流程
构建时 → 对每个路由预渲染静态 HTML
↓
部署到 CDN → 请求直接返回静态文件
内容不频繁变化时,SSG 比 SSR 服务器压力更小。
二、SSR 基本架构
2.1 同构应用
同一套 Vue 代码在服务端 和客户端各运行一次:
┌─────────────┐
│ Vue 组件 │
└──────┬──────┘
│
┌──────────┴──────────┐
↓ ↓
服务端 render 客户端 hydrate
renderToString createApp + mount
↓ ↓
HTML 字符串 可交互应用
2.2 服务端渲染示例
javascript
// server.js
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import App from './App.vue'
export async function render(url) {
const app = createSSRApp(App)
const html = await renderToString(app)
return html
}
javascript
// entry-client.js(客户端)
import { createSSRApp } from 'vue'
import App from './App.vue'
const app = createSSRApp(App)
app.mount('#app') // Hydration
2.3 返回给浏览器的 HTML 结构
html
<!DOCTYPE html>
<html>
<body>
<div id="app"><!-- 服务端渲染的 HTML --></div>
<script>window.__INITIAL_STATE__ = {...}</script>
<script src="/client.js"></script>
</body>
</html>
三、Hydration 客户端激活
3.1 是什么
Hydration(水合)是客户端 Vue 接管服务端 HTML 的过程:绑定事件、恢复响应式,使静态 HTML 变成可交互应用。
服务端 HTML(静态,无事件)
↓ 客户端 JS 加载
Vue 对比 VNode 与服务端 DOM
↓ 匹配成功
绑定事件监听、挂载组件实例
↓
完整 SPA
3.2 Hydration 不匹配
服务端与客户端渲染结果不一致时会报警告:
[Vue warn]: Hydration node mismatch
常见原因:
- 服务端与客户端数据不同(时间戳、random、locale)
- 使用了
window/document导致两端 HTML 不同 - 浏览器插件修改 DOM
- 无效 HTML 结构(如
<p>内嵌<div>)
后果:控制台警告,可能导致事件绑定失败、样式错乱。
解决 :保证同一份数据、同一份模板,两端 render 结果一致;动态内容放 ClientOnly 或 onMounted。
3.3 避免不匹配
vue
<script setup>
import { ref, onMounted } from 'vue'
// ❌ 服务端和客户端结果不同
const time = ref(new Date().toLocaleString())
// ✅ 仅在客户端设置
const time = ref('')
onMounted(() => {
time.value = new Date().toLocaleString()
})
</script>
四、数据预取与状态同步
4.1 问题
服务端 render 时需要数据;客户端 Hydration 后也需要相同数据,否则不匹配。
4.2 脱水与注水
服务端:
1. 预取 API 数据
2. render 进 HTML
3. 序列化 state → 嵌入 __INITIAL_STATE__
客户端:
1. 读取 __INITIAL_STATE__
2. 注入 Pinia / Vuex
3. Hydration(与服务端同一状态)
javascript
// 服务端
const pinia = createPinia()
const app = createSSRApp(App).use(pinia)
await router.push(url)
await prefetchData() // 路由级数据预取
const html = await renderToString(app)
const state = JSON.stringify(pinia.state.value)
// 嵌入模板
`<script>window.__INITIAL_STATE__=${state}</script>`
// 客户端
const pinia = createPinia()
if (window.__INITIAL_STATE__) {
pinia.state.value = window.__INITIAL_STATE__
}
app.use(pinia).mount('#app')
每个请求需独立 Pinia 实例,避免用户间状态污染。
五、Streaming SSR(Vue 3)
5.1 传统 SSR 的问题
renderToString 需等整页数据就绪才返回 HTML,慢接口拖慢 TTFB。
5.2 流式渲染
javascript
import { renderToNodeStream } from '@vue/server-renderer'
const stream = renderToNodeStream(app)
stream.pipe(response)
边渲染边发送 HTML,浏览器可更早解析和展示先就绪的部分,改善首字节时间。
六、SSR 开发注意事项
6.1 生命周期
| 钩子 | 服务端 | 客户端 |
|---|---|---|
setup 同步代码 |
✅ | ✅ |
onBeforeMount |
✅ | ✅ |
onMounted |
❌ | ✅ |
onServerPrefetch |
✅ | ❌ |
面试常考 :onMounted 只在客户端 执行。服务端专用数据预取用 onServerPrefetch。
javascript
import { onServerPrefetch, onMounted } from 'vue'
onServerPrefetch(async () => {
await store.fetchList() // 仅服务端,render 前完成
})
onMounted(() => {
initChart() // 仅客户端,可访问 DOM
})
6.2 浏览器 API
javascript
// ❌ SSR 中会报错
const width = window.innerWidth
document.title = 'xxx'
// ✅ 环境判断
if (import.meta.env.SSR) {
// 服务端逻辑
} else {
// 客户端逻辑
}
// ✅ 或放 onMounted
onMounted(() => {
document.title = 'xxx'
})
6.3 第三方库
仅支持浏览器的库(ECharts、地图 SDK)应在 onMounted 或 <ClientOnly> 中加载,避免服务端执行。
6.4 Teleport 与 SSR
Teleport 在 SSR 时默认原位渲染,客户端 hydration 后才传送到目标位置,可能短暂闪烁(详见《Teleport 传送门》)。
七、Nuxt 简介
7.1 为什么用 Nuxt
手写 SSR 需处理路由、数据预取、代码分割、Hydration 配置,复杂度高。Nuxt 是 Vue 官方 SSR 框架,开箱即用。
7.2 Nuxt 3 vs Nuxt 2
| 对比项 | Nuxt 2 | Nuxt 3 |
|---|---|---|
| Vue 版本 | Vue 2 | Vue 3 |
| API 风格 | Options 为主 | Composition API |
| 引擎 | 传统 Node 服务 | Nitro(边缘、Serverless 友好) |
| 渲染 | SSR / SPA | SSR / SSG / Hybrid |
| 数据获取 | asyncData / fetch |
useFetch / useAsyncData |
vue
<!-- Nuxt 3 页面 -->
<script setup>
const { data } = await useFetch('/api/posts')
</script>
<template>
<ul>
<li v-for="post in data" :key="post.id">{{ post.title }}</li>
</ul>
</template>
useFetch 在服务端预取,自动脱水/注水到客户端。
八、何时选 SSR
| 选 SSR | 选 CSR | 选 SSG |
|---|---|---|
| SEO 重要 | 纯后台系统 | 博客、文档 |
| 首屏要快 | 登录后应用 | 内容更新不频繁 |
| 社交分享预览 | 实时协作工具 | 营销落地页 |
SSR 代价:服务器成本、开发复杂度、需注意 Hydration 与环境差异。
九、面试聚焦
9.1 onMounted 只在客户端执行
服务端没有 DOM,mounted 类钩子仅客户端运行;服务端数据用 onServerPrefetch。
9.2 Hydration 不匹配会怎样?
控制台警告,可能导致事件未绑定、交互异常。需保证两端 render 一致。
9.3 SSR 中能用 window/document 吗?
不能直接在 setup 中用,会服务端报错或无定义。用 import.meta.env.SSR 判断或 onMounted。
9.4 SSR 和 SSG 区别?
SSR 每次请求在服务器渲染;SSG 构建时生成静态 HTML,部署后无需 Node 服务。
十、易混淆点
- SSR ≠ 不需要 JS:Hydration 后仍是 SPA,JS 必下载。
- SSR ≠ SSG:动态 SSR vs 构建时静态化。
- setup 两端都跑:不是只有客户端。
- 状态必须同步:脱水/注水失败 → Hydration mismatch。
- 每个请求独立 store:SSR 不能共享全局单例状态。
十一、思考与练习
1. SSR 的核心流程?
解析:服务端 renderToString 生成 HTML → 返回浏览器 → 客户端加载 JS → Hydration 激活。
2. 什么是 Hydration?
解析:客户端 Vue 接管服务端 HTML,绑定事件、恢复响应式,使页面可交互。
3. 为什么 SSR 里不能直接用 window?
解析:Node 服务端无 window,执行会报错;需环境判断或 onMounted。
4. INITIAL_STATE 的作用?
解析:服务端序列化状态嵌入 HTML,客户端读取后注入 store,保证 Hydration 数据一致。
5. 什么时候用 Nuxt 而不是手写 SSR?
解析:需要完整 SSR 工程(路由、数据、构建、部署)时,Nuxt 降低同构开发成本。
总结
- SSR:服务端渲染 HTML,提升首屏与 SEO;客户端 Hydration 激活
- CSR / SSG:纯客户端 vs 构建时静态化,按场景选型
- Hydration:两端 HTML 须一致,否则警告与交互异常
- 数据 :预取 + 脱水/注水(
__INITIAL_STATE__)同步状态 - 注意:onMounted 仅客户端、禁用 window/document、每请求独立 store
- Nuxt 3:Vue 3 + Nitro,useFetch 等简化 SSR 开发