Skip to content

02|水合 ​

第八章 / 02。目标:保证服务端输出与客户端第一次 render使用同一输入,而不是事后隐藏警告。

接管已有 DOM ​

服务器用 createSSRApp() 生成 HTML;浏览器解析后,客户端创建同一应用并 mount 到已有容器。Vue 逐步匹配组件与 DOM、恢复响应式关系并添加事件监听。此时才从“可读 HTML”变为“Vue 可交互界面”。

匹配的合同是结构和相关文本/属性一致。常见破坏者:

  • 模板含无效 HTML 嵌套,浏览器解析时自动改树;
  • render 中直接调用 Math.random() 或 Date.now();
  • 服务器与浏览器时区不同;
  • 首轮 render 读取 localStorage、窗口尺寸等浏览器专属输入;
  • 客户端又请求一次,首轮数据恰好已变化。

可重放错配与修复 ​

在 Nuxt 环境 的 app/pages/hydration.vue 中运行以下完整组件,直接刷新 /hydration,比较页面源代码与浏览器 console。错误版:

vue
<script setup lang="ts">
const requestId = Math.random().toString(16).slice(2)
</script>
<template><p>request: {{ requestId }}</p></template>

服务端和客户端各执行一次,值通常不同。Nuxt 修复版把首次值放进 payload:

vue
<script setup lang="ts">
const requestId = useState('request-id', () => crypto.randomUUID())
</script>
<template><p>request: {{ requestId }}</p></template>

在服务端初始化后,useState 的值被序列化给客户端;水合首轮复用同一值。key 要稳定且唯一,值必须可序列化。这个 request id 是公开演示值;令牌、密码、完整用户对象不能因为“方便复用”进入 payload。

浏览器专属显示可给两端一个相同占位,mounted 后再替换:

ts
const timezone = ref('UTC')
onMounted(() => {
  timezone.value = Intl.DateTimeFormat().resolvedOptions().timeZone
})

顺着两棵树走一次 ​

设响应中的 DOM 是 p → 文本 request: a1,客户端 render 得到的是 p → 文本 request: b2。标签类型相同让 Vue 能关联既有 p,但文本输入不同仍需要恢复;它不是因为“用了随机数 API”才错,而是两次执行得到不同事实。使用 payload 后,两端都是 a1,已有节点被接管,再由客户端事件触发后续更新。

删除 useState 改回普通局部初始化,警告应回来;禁用 JS 时只能看到服务端那一次结果,没有客户端事件。检查原始响应、首轮 DOM 与后续事件三个时间点,才能区分 SSR 已输出、水合一致与真正可交互。

如何定位而不是压制 ​

  1. 保存原始响应 HTML,不以水合后的 Elements 代替。
  2. 在客户端首轮 render 前记录同一数据 key 的值。
  3. 检查浏览器解析是否修正了无效嵌套。
  4. 固定随机种子、时区和接口响应,缩成最小组件。
  5. Vue 3.5 的 data-allow-mismatch 只标记已知且不可避免的局部差异;它不会同步状态,也不会消除恢复成本。

练习:让两端使用同一随机输入 ​

在 /hydration 新增按钮:初始显示 useState 保存的 requestId,点击后把它改为固定字符串 clicked。记录原始响应中的值、首次浏览器值、点击后值;前两者必须一致,第三者必须改变。再故意只在客户端初始化为 client-only,记录错配并恢复。普通练习不要求自行实现未讲授的伪随机算法。

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

用心记录每一步成长