Vue 服务端渲染 SSR

文章目录

  • 前言
  • 一、三种渲染模式
    • [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 结果一致;动态内容放 ClientOnlyonMounted

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 服务。


十、易混淆点

  1. SSR ≠ 不需要 JS:Hydration 后仍是 SPA,JS 必下载。
  2. SSR ≠ SSG:动态 SSR vs 构建时静态化。
  3. setup 两端都跑:不是只有客户端。
  4. 状态必须同步:脱水/注水失败 → Hydration mismatch。
  5. 每个请求独立 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 开发