Vuejs 错误: 客户端呈现的虚拟 DOM 树与服务器呈现的不匹配

我正在使用 Nuxt.js/Vuejs 作为我的应用程序,并且我一直在不同的地方面对这个错误:

    The client-side rendered virtual DOM tree is not matching server-rendered content.
This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>.
Bailing hydration and performing full client-side render.

我想知道调试这个错误的最好方法是什么?它们是我可以为客户端和服务器记录/获取虚拟 DOM 树的一种方法吗? 这样我就可以比较并找出错误所在?

矿山是一个大型的应用程序,手工验证困难。

79926 次浏览

部分答案是: 通过 Chrome DevTools,你可以将问题本地化,并确切地看到是什么元素导致了这个问题。执行以下操作(我使用 Nuxt5.6.0和 Chrome64.0.3282.186完成了该操作)

  1. 在 Chrome 中显示 DevTools (F12)
  2. 加载导致“客户端呈现的虚拟 DOM 树...”警告的页面。
  3. 滚动到 DevTools 控制台中的警告。
  4. 单击警告的源位置超链接(在我的示例中是 vue.runtime.esm.js: 574)。
  5. 在那里设置一个断点(在源代码浏览器的行号处左键单击)。
  6. 再次显示相同的警告。我并不是说它总是可能的,但在我的情况下,我只是重新加载了页面。如果有许多警告,您可以通过将鼠标移动到 msg变量上来检查消息。
  7. 当您找到消息并停在断点上时,请查看调用堆栈。单击一个框架下调用“补丁”打开其源代码。将鼠标悬停在 patch中执行线上方的 hydrate函数调用4行上。将打开到 hydrate源的超链接。
  8. hydrate函数中,从开始移动大约15行,并在 assertNodeMatch返回 false之后返回 false的位置设置一个断点。在那里设置断点并删除所有其他断点。
  9. 再次发出同样的警告。现在,当命中断点时,执行应该在 hydrate函数中停止。切换到 DevTools 控制台并计算 elmvnode。在这里,elm 似乎是一个服务器呈现的 DOM 元素,而 vnode 是一个虚拟 DOM 节点。榆树是以 HTML 格式打印出来的,因此您可以找出错误发生的位置。

这里有一个关于如何处理修改 DOM 的集成(例如 Google Analytics 或 FB Pixel)的示例。基本上创建一个插件,并从 SSR 中排除。

Https://nuxtjs.org/faq/ga

好吧,这听起来有点傻。我花了大约15分钟尝试了一系列不同的解决方案,比如重新启动服务器并删除。但我懒得使用@budden73的 big brain 解决方案。最后为我工作的只是简单的 重启我的电脑,给它一个机会。

到目前为止,我从观察中发现,当您使用第三方包(特别是 jQuery)时,它们有时会将 html 标记注入 dom 中。于是 Vue/Nuxt 跟丢了 Dom 树,开始抱怨。

我也遇到了同样的问题,过了一段时间,我删除了所有 jQuery,用 Vuejs 替换了 jQuery 的功能,这些错误都消失了。

对我来说,这个错误发生的原因是在 AsyncData获得数组列表,并渲染 <tr>标签的 v-for,我把 v-for代码在 <client-only>块和问题解决了

这个错误对于调试来说非常痛苦。为了快速获得导致问题的元素,编辑 node_modules/vue/dist/vue.esm.js并添加以下代码行:

// Search for this line:
function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {
var i;
var tag = vnode.tag;
var data = vnode.data;
var children = vnode.children;
inVPre = inVPre || (data && data.pre);
vnode.elm = elm;


// Add the following lines:
console.log('elm', elm)
console.log('vnode', vnode)
console.log('inVpre', inVPre)
// ...




您将在控制台中获得故障节点。

结果,在我的情况下,我有 HTML 注释标签,这导致了这个愚蠢的,恼人的错误。我花了很长时间才弄明白,但是以防万一。

对于 Nuxt 2.10以上的版本,它不需要安装任何东西,只需使用默认的组件 <client-only>,就像前面提到的 https://nuxtjs.org/api/components-client-only/

检查之前的警告:

"nuxt": "^2.12.2"中,可以很容易地从前面的警告中找到原因。

enter image description here

就我而言:

不对

<nuxt-link to="/game42day">
<a>Game For Today</a>
</nuxt-link>

正确:

<nuxt-link to="/game42day">
Game For Today
</nuxt-link>

在实现 Vue 粒子包时,我遇到了与 nuxt 版本 2.14.0相同的问题。解决办法是用 no-ssr包围标记,这样就解决了问题。

编辑:
解决方案的更新版本(如果 Nuxt 版本高于 2.9.0)

<client-only>
<vue-particles>
</vue-particles>
</client-only>

老办法:

<no-ssr>
<vue-particles>
</vue-particles>
</no-ssr>

就我而言,我不得不改变这一点:

<v-expansion-panel-header v-text="name" />

回到这里:

<v-expansion-panel-header>\{\{ name }}</v-expansion-panel-header>

那么:

extend (config, ctx) {
config.resolve.symlinks = false
}

看这个 [ Vue 警告] : 客户端呈现的虚拟 DOM 树不匹配服务器呈现的内容(Nuxt/Vue/lerna monorepo)

如果您使用 v-if有条件地呈现一个组件,那么您有两个选择来解决这个问题:

第一个是在 <no-ssr></no-ssr>标记中包装元素。

第二种方法是用 v-show代替 v-if给你是到 Vue 文档的链接。

有很多方法可以解决这个问题,但大多数都不是真正的解决办法,只是一些粗糙的创可贴。注意以下几点:

  • 把它包装成 <client-only>标签,小心一些 重要的细节 tho
  • 使用 v-show而不是 v-if
  • 试图黑进一些生命周期
  • 等等。

我强烈推荐阅读亚历山大 · 利希特写的这篇华丽的文章

Https://blog.lichter.io/posts/vue-hydration-error/

他会向你解释,你应该诊断为什么会发生这种情况,并解决实际问题。
基本上每次都有不同于 在服务器上生成在客户身上补充水分的时候可以使用的地方会导致这个错误。

其中包括:

  • 无效的 HTML (在 <p>中有一个块元素,嵌套在另一个 a标记中的情况也一样,等等)
  • 第三方脚本扰乱你的组件
  • 服务器和客户端的不同状态
  • 任何随机都是有风险的(例如 new Date())
  • 任何与认证有关的网页

我强烈推荐阅读这篇文章,用亚历山大自己的话来理解如何处理这类问题。如果您赶时间,您总是可以使用一个创可贴修复,但尝试实际修复问题,以获得最佳性能,并保持代码整洁。

检查内联元素中是否使用了任何块级元素。

例如: 内部,内部

如果使用了 HTML 表,请确保使用了标记

由于这个问题,我也得到了很多错误。我列举了两个我经常遇到的情况,希望能对你有所帮助。

  • 使用 vuetify 按钮,当您创建一个公共组件时,您应该使用: <v-btn>\{\{text}}</v-btn>:
<template>
<v-btn
:width="width"
:color="color"
:class="[rounded ? 'rounded-pill' : 'rounded-lg',textColor]"
v-on:click="onClick"
elevation="0"
:outlined="outlined"
:type="type"
:name="name"
:form="form"
:disabled="disabled"
v-bind="$attrs"
>\{\{ text }}</v-btn>
</template>
  • 不要使用带有 <p>标记的 v-html。 不使用: <p v-html='html'></p>。 用途: <div v-html='html'></div>

此外,如果你使用 <client-only></client-only>,这个问题肯定是解决了,但如果你需要搜索引擎优化页面或显示谷歌广告,这是不好的解决方案。

在我的情况下,我改变了我的代码

<p v-html="$md.render(post.content)"></p>

<p>\{\{ $md.render(post.content) }}</p>

感谢 Budden73的回答,我对调试过程做了一些改进。

  1. 打开开发工具
  2. 点击 warn消息,然后点击警告消息的第一行,您将被引导到 Sources面板,文件名为 vue.runtime.esm.js?xxxx

enter image description here

  1. 在上面的文件中搜索 assertNodeMatch,不是函数,而是类似于:
    if (process.env.NODE_ENV !== 'production') {
if (!assertNodeMatch(elm, vnode, inVPre)) {
return false
}
}
  1. return false行添加一个断点

enter image description here

  1. 刷新页面,将触发断点。
  2. 在 Source 面板的右侧,在 Scope->Local下,单击 elm元素,您将被引导回到 Elements面板。

enter image description here

  1. 上面的元素是 client side渲染的元素,与您的代码进行比较可以看出差异。

如果找不到 bug 的源头,最残忍的解决方法是使用 nuxt 的 <client-only>标记。

另一种可能残酷的方式是描述 给你。添加一个默认为 false 的 isHydrate变量,在 mounted钩子中设置为 true,并在变量设置为 true 后呈现元素。

既然已经找到了导致问题的代码,那么首先应该做的是验证您的标记(可能来自 API)是否有效。像 <p><p>Text</p></p>这样的代码是无效的,因为 p 元素不允许其他块元素(比如段落标记)包含在内。

请注意,标记不允许将类似 <div><p>的块级元素作为子元素。不过,这些 <span>标记是 Vue 转换的默认标记。你可以通过 <Transition tag="div">改变它。