SSR 的静态 HTML 变成交互应用的过程
恢复交互能力:
// 服务端返回的只是死 HTML
<div>
<button>点击我</button> // ❌ 点了没反应
</div>
// 水合后变成活应用
<div>
<button @click="handleClick">点击我</button> // ✅ 可以交互
</div>接管响应式数据:
// 水合前:显示初始计数 0
<button>0</button>
// 水合后:Vue/React 接管数据,点击可以增加
const count = ref(0) // 恢复到 0
<button @click="count++">{{ count }}</button>同步组件状态:
// 服务端创建了某个状态
<div class="user-login">欢迎, 张三</div>
// 水合时恢复对应的 JS 状态
const user = ref({ name: '张三' }) // 恢复状态
// 后续操作基于这个状态继续Nuxt 完整保留了 Vue 的所有生命周期钩子(如 beforeCreate、mounted 等),但关键在于它们运行的环境不同:
Nuxt 特有的生命周期钩子(在服务端运行):
| 钩子 | 运行环境 | 主要作用 |
|---|---|---|
nuxtServerInit | 服务端 | 在服务端渲染前,用于初始化 Vuex 状态,例如将服务端获取的用户信息存入 store。 |
middleware | 服务端/客户端 | 在进入页面或布局之前执行,常用于路由验证、重定向等。 |
validate() | 服务端 | 校验动态路由参数是否有效,若无效可返回 404 页面。 |
asyncData | 服务端/客户端 | 最重要的钩子之一,用于在页面组件加载前异步获取数据,并将返回值合并到组件的数据中。它在服务端执行后,会在客户端水合前再次执行。 |
fetch | 服务端/客户端 | 用于在页面渲染前填充 Vuex store 的状态。 |
第一步:浏览器请求 → Node.js 服务端
当用户首次访问你的 Nuxt 应用时,请求会先到达 Node.js 服务器。服务器会执行一系列操作,包括运行nuxtServerInit、middleware、以及页面组件中的asyncData和fetch函数来获取所有需要的数据。
第二步:服务端 → 完全静态的 HTML
服务器利用获取到的数据,将 Vue 组件“渲染”成一个完整的 HTML 字符串,然后作为响应发送回浏览器。这时,用户能立即看到页面的完整内容,这就是 SSR 带来的首屏加载优势。
第三步:浏览器接收 → 水合 (Hydration)
水合是连接“服务端渲染的静态页面”和“客户端动态应用”的桥梁,它发生在浏览器下载并执行完所有必要的 JS 代码之后
水合过程中,最大的坑就是服务端生成的 HTML 和客户端 Vue 期望生成的 HTML 结构不一致。这会导致 Vue 被迫放弃服务端渲染的 DOM 树,重新在客户端渲染,引发性能问题、交互失效,甚至页面闪烁
全量水合的问题:
// 传统方式:所有组件一起水合
页面加载 (0ms)
↓
下载所有 JS (300ms)
↓
同时水合 7 个组件 (500ms) ← 主线程完全阻塞
↓
页面可交互 (800ms) ❌ 用户需要等待 800ms 才能点击按钮水合过程:
// 服务端返回的静态 HTML
<div id="app">
<button class="counter">0</button>
</div>
// 水合过程(简化版)
function hydrate() {
// 1. 遍历整个 DOM 树(CPU 密集)
const buttons = document.querySelectorAll('button')
// 2. 为每个元素绑定事件(CPU 密集)
buttons.forEach(button => {
// 3. 解析指令、建立响应式连接(CPU 密集)
button.addEventListener('click', () => {
// 更新数据、重新渲染...
})
// 4. 恢复组件状态(CPU 密集)
const initialValue = button.textContent
// 建立 Vue 的响应式系统...
})
}