Vue-SSR 客服端激活失败(Vue hydration fails)

这是我参与更文挑战的第 16 天,活动详情查看: 更文挑战

Lynne,一个能哭爱笑永远少女心的前端开发工程师。身处互联网浪潮之中,热爱生活与技术。

前言

前一阵遇到了一个Bug…..

Error: [nuxt] Error while mounting app: HierarchyRequestError: Failed to execute 'appendChild' on 'Node':
This node type does not support this method. at some-file.js:1
复制代码

整整排查了2个多小时,如果要追究其根本原因,找到了这篇文章,解释完美。

原文地址:Vue激活失败(blog.lichter.io/posts/vue-h…

服务端渲染有很多好处,特别是当像Nuxt.js或GridSome这样的网站,无论是使用动态SSR还是生成静态网站,开发 Vue-SSR 应用程序都是一件轻而易举的事。但从另一方面来讲,服务端渲染也会带来从未见过的复杂性和错误。尽管大多数错误都被记录在案且提供了变通的解决方案,但一个错误仍让很多人困惑:Vue激活失败。

一、什么是 Vue激活失败

激活是当Vue转换服务端渲染标记并使之反应的过程,因此使它能反映 Vue 的变化。 如果Vue期待与渲染的HTML不同的标记,就会发生激活失败。

在Vue-SSR中是这样解释的:Vue 在浏览器端接管由服务端发送的静态HTML标记,并使其变为由Vue管理的动态DOM的过程。

理解:服务端已经渲染好了HTML,无需将其丢弃再重新创建所有的 DOM元素,而是去激活这些渲染好的静态HTML,使他们成为动态的以能够响应后续的数据变化。

SSR + 客户端混合 — 浏览器会更改一些特殊的HTML结构,导致与Vue生成的虚拟DOM结构不匹配。

二、如何辨认激活失败

我们现在意识到激活是什么和在什么时候会失败,但是我们作为开发者如何发现激活没有如预期一般工作呢?有两条错误消息肯定会指出激活失败但都有限制条件。

1. 第一条是仅在开发中出现,无论哪种模式:

Parent:  <div class="container"> client-hook-3.js:1:16358
Mismatching childNodes vs. VNodes: NodeList(3) [ p, p, p ]  Array [ {…} ]
    
[Vue warn]: 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.
复制代码
  1. 第二条错误消息是仅在生产环境和静态生成站点时:
Error: [nuxt] Error while mounting app: HierarchyRequestError: Failed to execute 'appendChild' on 'Node':
This node type does not support this method. at some-file.js:1
复制代码

众所周知,激活仅在页面首先由服务端渲染时发生,因此通常仅在你应用的初始化请求中。

(asyncData 及 data中—客户端的data会和asyncData中的data混合)

因为当通过一个标签导航时激活失败是不可见的而仅在硬重载时才可见,这使得发现激活失败问题变得更加困难。

因此激活错误有时仅在分级系统或者更糟,仅在生产环境下被发现。在极少数情况下,甚至不会打印出错误而仅仅时某些组件停止工作。

三、一般引发错误的原因

现在我们了解了如何发现激活错误,我们将研究导致Vue激活错误的典型原因。当然我们不可能覆盖所有可能的原因,因为它们差别很大,而且主要取决于你的代码。

在以下章节中,每次提到服务端渲染,它就与两种情况都相关(动态SSR和静态站点生成),因为从技术上讲,两者都具有服务端渲染内容。 (除非另有声明)

1. 不合理的HTML

当激活失败发生时不合理的HTML是你应该检查的第一个地方,这也应该是有错误信息提示的:

This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>
复制代码

不幸的是,不合理的HTML通常不是激活失败的原因。不过,你应该仔细检查你的标记。另外,你还要确保检查缩小设置,因为过度的HTML缩小可能会导致无效的HTML。如果您有用户生成的输出或来自CMS的内容,则值得验证此内容也是有效的HTML。最后,第三方插件或服务也可能影响和操纵HTML。后者的一个常见示例是Cloudflare,当您启用了它们的服务。如HTML缩小,Rocket loader或其他更改页面内容的功能时。

我创建了一个简单的示例codeandbox,其中包含无效的HTML并触发了激活失败。

2. 修改HTML的脚本

关于脚本:如果你向你的Vue应用中插入第三方JS文件,也可以在Vue接收并激活来自服务器的HTML之前更改HTML

3. 服务器和客户端的状态不同

服务器和客户端上状态不一致是发生激活失败最常见的原因。像往常一样,不一致的原因千差万别。

1)日期、时间戳和随机化

当您的网站包含日期或者时间戳时,应尽可能小心并使其尽可能静态,尤其在您的网站是静态生成的情况下。如果客户端评估像new Date() 这样的表达式,则该表达式可能会与在你服务器上开发阶段检索相同日期时生成的日期不同。这也让我对公司的“关于”页面感到困惑,在该页面上,我想根据当前分钟对显示的人员进行排序。

export const deterministicRotate = (arr) => {
  if (arr.length <= 1) {
    return arr
  }
  const rotations = (new Date()).getMinutes() % arr.length
  return rotations ? arr : arr.reverse()
}
复制代码

如果用户打开页面的时间很奇怪,则计划将阵列反转。当使用动态SSR时效果很好。但当切换到静态生成的JAMstack站点时,该功能就会成为一个Bug。你可以在一分钟后点击链接刷新,会发现名字和人正确的交换了,但图片和原来一致。糟糕!这是由于服务器和客户端时间不匹配导致的。在移除不确定型洗牌代码后工作恢复正常。

2)授权

不一致的另一个常见原因是用户身份验证。这适用于动态SSR和静态站点生成。

当仅在客户端(例如,在localStorage中)上存储身份验证状态时,服务器“不知道身份验证”。这将不可避免地导致激活问题,因为登录时服务器和客户端信息根本不同。因此,如果服务器不知道正在静态生成您的页面的身份验证状态,则不应在服务器端呈现任何与身份验证相关的组件。

您可能想知道为什么它总是适用于静态网站:因为当您生成网站时,它是HTML,而序列化的代码是“无状态的”。在构建阶段,我们无法考虑“已登录的用户状态”。这意味着您必须从服务器上的渲染中排除所有与身份验证相关的组件。

3)其他原因

除了这两种情况外,还有更多边缘情况可能会打击您并引起不一致。即使未在此处列出,我们也将解决激活错误!首先,我们将其范围缩小到导致问题的DOM元素。

四、解决激活失败问题

1. 发现导致激活失败的元素

我们可以使用您最喜爱的浏览器上的devTools缩小问题到一个特定的组件或者DOM元素。

1)确保你在开发环境下

2)打开开发调试工具

3)触发激活警告(通常通过重载网页)

4)展开 [Vue Warn] The client side … 错误消息查看追踪堆栈(取决于浏览器,也打开弹出的VueJS列表)

5)点击一个激活回调,将会打开Vue激活函数的源代码

6)现在,无论何时这个函数返回false都设置一个debugger,在撰写文本时,这种情况发生了三遍:

if (process.env.NODE_ENV !== 'production') {
  if (!assertNodeMatch(elm, vnode, inVPre)) {
      return false //HERE
  }
}
if (process.env.NODE_ENV !== 'production' &&
          typeof console !== 'undefined' &&
          !hydrationBailed
    ) {
        hydrationBailed = true;
        console.warn('Parent: ', elm);
        console.warn('server innerHTML: ', i);
        console.warn('client innerHTML: ', elm.innerHTML);
    }
  return false //HERE
}
 if (process.env.NODE_ENV !== 'production' &&
      typeof console !== 'undefined' &&
      !hydrationBailed
  ) {
    hydrationBailed = true;
    console.warn('Parent: ', elm);
    console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children);
  }
  return false //HERE
}
复制代码

这同样允许在激活失败前检查激活函数的参数。

7)最后但同样重要的是,让激活错误复现,通常再次重载页面是有可能的但有时更困难。

8)你现在看到触发了我们的一个断点,脚本停止执行了

9)现在打开调试工具的控制台,在激活失败的地方写一个元素去获取DOM元素。使用DOM元素,你将能够将激活错误追溯到你的Vue组件之一

10)继续执行下一步

PS:这是用户budden73对此StackOverflow答案的改编工作流。

2. 确保你的HTML标记合理

现在你发现了导致问题的代码,你首先要做的事确保你的标记(也许来自一个API)是合理的。像这样的代码

Text

无效,因为一个p元素不允许在其中包含其他块元素(如段落标签)。

但是注意,标记不允许像

这样的标签作为子元素。这些标记是Vue过渡的默认标记。你可以通过进行改变。

3. 解决服务器与客户端之间的不一致

在debugger期间,你能够从服务器看到结果和重新绘制客户端侧。如果存在不同,你可以看一看,你如何获取数据和你在服务端或客户端渲染了什么。一个常见的问题是静态网页的认证。因为HTML在构建时生成的是无状态的,因此不知道任何授权状态,你应用的所有和授权有关的部分都应该旨在客户端重新渲染。否则,在客户端有授权状态的用户,因为登录而期望从服务端获取不同的HTML。然后只剩下一个选项…

4. 最终避免措施:

最后一个解决激活错误的选择是完全避免组件出现激活错误。这对于在静态生成的页面上与身份验证相关的组件来说是必须的,有时对于交付您不能更改但必须嵌入的内容的组件(例如,来自第三方应用程序)也是必需的。

正如我们在一开始了解到的,激活仅发生在组件被同时渲染在服务端和客户端时。为了避免激活失败,我们通过标签避免重新渲染服务端组件。
唯一的缺点:该组件不包含在服务器返回的HTML中,对SEO没有帮助。

五、总结

让我们结束吧,现在你了解更多:

什么是激活以及它做了什么?

激活怎么失败的以及如何发现激活失败?

激活失败的一般原因

如何调试激活失败及解决你的应用程序

我希望这篇文章很有见识,并且您学到了一两件事。您是否遇到了此处未描述的激活错误原因,或者我错过了一个常见原因?随时在Twitter上或通过邮件给我发消息。
而且像往常一样-如果您能宣传并与同事分享博客文章,我将很高兴。

总结

看完这篇文章收获很大,希望你也是~~~

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享