客户端水化
提示(译者批注,非官方文档内容)
理解本页面的前提是您得知道:由 SSR 服务端返回的 html 是静态的,不可以进行交互,所以才需要下文中的水化步骤使静态的页面变成动态 DOM。
Hydration (水化/水合作用)是指客户端中,Vue 接管由服务器发送的静态 HTML,并将其转换为动态 DOM 的过程,使其可以对客户端数据变化做出反应。
由于服务器已经渲染了静态的 html 标签,我们显然不想将其丢弃并重新创建所有 DOM 元素。取而代之的是,我们想"hydrate"(补充水分/水化/水合)静态标签并使其具有交互性。
警告
在开发模式下,Vue 将断言客户端生成的虚拟 DOM 树与从服务器呈现的 DOM 结构匹配。如果存在不匹配,它将放弃现有 DOM 并从头开始渲染。在生产模式下,为了获得最佳性能,将禁用此断言。
激活不匹配
在使用 SSR + 客户端水化时,有一件事情需要注意:使用一些特殊的 HTML 时要符合浏览器的 HTML 规范,如果组件模板中存在不符合规范的 HTML 结构,那么渲染后的 HTML 会被浏览器原生的 HTML 解析行为纠正,导致不匹配。例如,当您在 Vue 的模板中写入以下代码时:
<table>
<tr><td>hi</td></tr>
</table>
浏览器的解析行为会自动在 <table>
中注入 <tbody>
标签。然而 Vue 的虚拟 DOM 生成的 HTML 却不包含 <tbody>
标签,所以会造成不匹配,为确保激活匹配,请书写标准的 HTML 模板。
提示(夹带私货非官方文档内容)
激活不匹配还需要注意另外两件事情:
- 渲染所用的数据中包含随机生成的值。由于同一个应用会在服务端和客户端执行两次,每次执行生成的随机数都不能保证相同。避免随机数不匹配有两种选择:
- 利用 v-if + onMounted 让需要用到随机数的模板只在客户端渲染。您所用的上层框架可能也会提供简化这个用例的内置 API,比如 VitePress 的
<ClientOnly>
组件。 - 使用一个能够接受随机种子的随机数生成库,并确保服务端和客户端使用同样的随机数种子 (比如把种子包含在序列化的状态中,然后在客户端取回)。
- 服务端和客户端的时区不一致。有时候我们可能会想要把一个时间转换为用户的当地时间,但在服务端的时区跟用户的时区可能并不一致,我们也并不能可靠的在服务端预先知道用户的时区。这种情况下,当地时间的转换也应该作为纯客户端逻辑去执行。
当 Vue 遇到激活不匹配时,它将尝试自动恢复并调整预渲染的 DOM 以匹配客户端的状态。这将导致一些渲染性能的损失,因为需要丢弃不匹配的节点并渲染新的节点,但大多数情况下,应用应该会如预期一样继续工作。尽管如此,最好还是在开发过程中发现并避免激活不匹配。
处理水化错误
如果确实收到了一个水化失败的报错(列如:“Vuejs Error - The client-side rendered virtual DOM tree is not matching server-rendered content”) 可以尝试以下步骤:
- 打开浏览器的调试面板 (F12)
- 加载导致报错的页面(“the client-side rendered virtual DOM tree…”)
- 在调试面板的控制台中找到报错。
- 点击报错中的源代码的路径链接,进入 vue.runtime.esm.js.
- 在报错处设置一个断点(点击行号的左边)。
- 复现错误,通常刷新页面就可以复现,报错时将鼠标移到
msg
变量上可以得到详细的信息。 - 当您得到信息,并停在断点处,查看_call stack_(函数调用栈)单击下一帧调用“patch”以打开其源代码,鼠标移到 hydrate 函数上,打开 hydrate 函数的源码。
- 在 hydrate 函数的源码中,从开始往下大概 15 行的地方,可以看到
assertNodeMatch
函数返回false
,在此处设置一个断点,并移除其他的断点。 - 重新复现错误,然后发现断点生效,代码执行停在 hydrate 函数中。切换到调试面板的控制台,依次输入
elm
和vnode
命令。这里elm
是服务端渲染的 DOM,vnode
是 Vue 生成的虚拟 DOM,您可以对比它们的不同然后排查错误。