人们通常将水合错误描述为服务器生成的HTML与浏览器生成的HTML不匹配,但这并不是故事的全部。
为了解释这一点,让我们看看 React 的服务器端渲染是如何工作的。大多数实现都遵循相同的步骤。
由于 React 正在将它生成的 HTML 与浏览器显示的 HTML 进行比较,因此在服务器发送它和 React 开始运行的时间之间更改 HTML 的任何内容都可能导致水合错误。
水合错误的主要症状是 React 会放弃其服务器渲染的内容并执行完整的客户端渲染。这可能会导致页面闪烁并完全重新获取数据。
在开发中,您将看到稍微有用的错误消息。
在我们继续讨论水合错误的常见原因之前,一个超级方便的技巧是将服务器发送的 HTML 与 React 生成的 HTML 进行差异。
有三个地方可以寻找这个。
有许多在线HTML比较工具,只需谷歌一个并从三个来源中的两个粘贴HTML即可。
浏览器扩展程序通常有权修改任何网站(包括您的应用)的实时页面内容。如果扩展在 React 有机会比较它之前修改了 HTML,这可能会导致水化错误。
在隐身/无痕浏览窗口中试用您的应用,并禁用所有扩展程序。如果错误消失,您就知道扩展程序导致了问题,并且您可能对此无能为力。
还有一些桌面级广告拦截器和安全软件可能会导致类似的问题。如果您正在运行其中之一,请尝试禁用它或在另一台设备上进行测试。您可以在工具中为您的网站添加例外。
浏览器会进行大量纠错,以确保即使HTML格式不正确,页面也能合理呈现。
例如,如果您非法嵌套在
,浏览器将移动 的
外部以使 HTML 有效。
HTML 中禁止嵌套表单,因此,如果表单中有表单,浏览器会将嵌套表单移动到父表单之外,成为其同级表单。
当 React 补水时,它会将它生成的 HTML 与浏览器显示的 HTML 进行比较。如果浏览器移动元素,React 将抛出水化错误。
这里的解决方案是编写有效的 HTML。
第三方脚本也可能修改页面上的 HTML。这在HotJar或Google Tag Manager(GTM)等分析脚本中尤其常见。如果您使用的是第三方脚本,请尝试将其删除,看看错误是否消失。
如果这是问题所在,您可以在 React 完成补水后运行脚本。例如,可以使用 在 useEffect 第一次渲染后运行脚本。
CSS 中的 CSS 是一种设置 React 应用程序样式的方法,它将样式定义为渲染的副作用。这意味着当服务器呈现页面时,样式不可用,并且服务器发送到客户端的 HTML 将与 React 生成的 HTML 不匹配。
Chakra、Emotion 和 Material UI 是 JS 库中流行的 CSS,存在此问题。
字符编码问题通常会显示明显的错误消息,因此易于诊断。
[Error] Warning: Text content did not match.
Server: "â€"
Client: "’"
当服务器和客户端的字符编码不匹配时,会发生这种情况。最常见的原因是服务器正在发送 UTF-8 编码的文本,但客户端将其解释为 ISO-8859-1。
要解决此问题,请将以下元标记添加到 HTML 中。
<meta
http-equiv="Content-Type"
content="text/html;charset=utf-8"
/>
可能最臭名昭著的罪魁祸首是 Date 对象。约会很复杂,你越少处理它们,你的生活就会越快乐。
当javascript尝试创建日期时,它将使用运行它的机器的时区。这意味着,如果您在服务器上创建一个日期并将其发送到客户端,客户端将在其自己的时区中创建一个日期,该时区将与服务器的时区不同。
如果您尝试呈现日期,则当服务器日期与客户端日期不同时,您每天都会在接近午夜时遇到这些补水问题。
如果您使用的是非幂等函数(例如 uuid 生成唯一 ID),则可能会收到冻结错误。这是因为服务器和客户端将生成不同的 ID。
您可以在服务器上生成 ID 并发送到客户端,也可以将种子传递给函数,以便服务器和客户端生成相同的 ID。
某些数据仅存在于客户端上,在服务器首次呈现页面时将不可用。
例如,您可能有一个从本地存储中获取默认值的输入。如果在服务器上将输入呈现为空,然后在客户端上填充它,则会出现冻结错误和难看的空输入闪烁。
服务器无法知道客户端想要渲染的内容,因此您的解决方案是找到一种方法来隐藏服务器上和初始水合渲染期间的元素。
来自Remix Utils的useHydrated钩子将为您提供一个 isHydrated 布尔值,您可以使用该布尔值有条件地渲染元素。
This is the easiest solution but has its own caveats
这是最简单的解决方案,但有其自身的注意事项
我的首选解决方案是使用 ProgressiveClientOnly 组件,该组件将使用 CSS 隐藏服务器呈现的内容,并在 Javascript 可用时将其交换为客户端内容,否则它将显示服务器内容。
对于服务器端渲染的所有好处,也有一些事情变得更加困难,例如在客户端和服务器中显示相同的日期。
每当服务器可以渲染的内容与客户端可以呈现的内容不匹配时,您都会遇到问题。
处理不当的网站最终会出现无样式内容(FOUC)或不正确内容的闪烁(FOIC)。您可能会遇到补水错误,并导致您的网站完全依赖服务器端渲染。
有些人试图通过仅在客户端上呈现某些元素来解决此问题。这是避免不匹配的可靠方法,但这也意味着没有javascript的用户将永远不会看到该内容。
没有javascript的用户将获得服务器上呈现的任何版本的页面,因此,如果要逐步增强,服务器必须呈现内容。
也就是说,javascript将适用于大多数网站的大多数用户,因此我们想要针对这种情况进行优化。
我们希望确保该网站在没有Javascript的情况下运行,但是如果这意味着该网站对大多数用户来说效果很好,那么我可以让这种体验有点笨拙。
由于我们必须在服务器上呈现内容,但我们不想立即向用户显示它,因此我们需要以某种方式隐藏它。Javascript解决方案在这里不起作用。即使你告诉 React 立即隐藏它,在 React 接管之前,服务器渲染页面仍然会有闪光。
我们剩下的是一个CSS解决方案。
我构建了一个自定义包装器组件,该组件隐藏其内容,直到页面加载。如果禁用JS,它将通过CSS动画显示它们。如果没有,则可以通过传入类名来自定义行为。
import { useHydrated } from "remix-utils"
export function ProgressiveClientOnly({
children,
className = "",
}: {
children: React.ReactNode | (() => React.ReactNode)
className: string
}) {
const isHydrated = useHydrated()
return (
<div
className={
// Create this class in your tailwind config
isHydrated ? className : "animate-appear"
}
>
{typeof children === "function"
? children()
: children}
</div>
)
}
该 animate-appear 类是一个自定义 CSS 动画,它使元素在延迟后突然出现。
module.exports = {
theme: {
extend: {
animation: {
appear: "appear 300ms",
},
keyframes: {
appear: {
"0%, 99%": {
height: "0",
width: "0",
opacity: "0",
},
"100%": {
height: "auto",
width: "auto",
opacity: "1",
},
},
},
},
},
}