深入探讨 Web 开发中的预渲染和 Hydration

Next.jsGatsby.jsRemix这样的框架大家或多或少使用过,但是它们具体是如何工作的呢?

这些框架运用了预渲染(Pre-rendering)和 Hydration 等技术来构建高性能应用程序。在本文中,我们将讨论预渲染和 Hydration,以及为什么在构建单页面应用程序时它们是很重要的特性。为了理解这些概念,我们需要探究它们为什么被创建以及它们试图解决的问题

过去的 Web 开发:传统的 SSR

在传统 SSR 的时代,渲染和交互性是分开的。我们使用像Node.jsPHPJavaRuby on Rails这样的服务器端语言。
在我们的服务器中,我们使用像JSPEJS这样的模板语言创建了视图。视图就是 HTML 页面,我们可以在其中注入 JavaScript 或 Java 来添加功能、从数据库查询中获取动态数据以及使用像JQuery这样的语言创建交互部分。

传统 SSR 的缺点

  1. 性能问题
  • 每次用户请求一个页面时,都需要向服务器发出请求

    • 这意味着会有一个整页重载。

    • 复杂的查询可能会导致速度变慢。

  1. 可扩展性
  • 全球覆盖:需要一个动态 CDN来缓存我们的动态文件。CDN 更适合静态内容

  • 升级服务器:如果更多的用户开始使用该应用程序,服务器的需求就会增加。可能需要在资源上投入更多,例如通过添加更多服务器来进行扩展。

  1. 重复逻辑
  • 我们可能会有重复的代码。例如,如果我们试图验证表单字段,我们就必须在 EJS 文件和您的 API 端点中都进行验证。

让我们看一下下面的代码片段,以了解这种重复逻辑的一个示例:

EJS 中的代码:

<form action="/submit-form" method="POST" id="myForm">
  <label for="email">电子邮件:</label>
  <input type="email" id="email" name="email" />
  <button type="submit">提交</button>
</form>

<script>
  document
    .getElementById("myForm")
    .addEventListener("submit", function (event) {
      const email = document.getElementById("email").value;

      if (!email.includes("@")) {
        alert("请输入有效的电子邮件。");
        event.preventDefault();
      }
    });
</script>

Express.js 中的代码:

import express from "express";
const app = express();
const path = require("path");
const port = 3000;

// 用于接收表单数据
app.use(
  express.urlencoded({
    extended: true,
  })
);

// 视图引擎设置。需要一个名为 views 的文件夹
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");

app.get("/", (req, res) => {
  // 用于渲染视图
  res.render("index", { errors: null });
});

app.post("/submit-form", (req, res) => {
  const email = req.body.email;

  if (!email.includes("@")) {
    res.status(400).send("无效的电子邮件。");
    return;
  }
  // 继续进行表单处理
});

app.listen(port, () => {
  console.log(`沙盒正在端口 ${port} 上监听`);
});

传统的 SSR 存在显著的缺点,但单页面应用程序的出现标志着 Web 开发的新时代。

传统 SSR 与单页面应用程序

什么是单页面应用程序(SPA)?

单页面应用程序(SPA)是一种网络应用程序的实现方式,它只加载一个单一的网络文档,然后当需要显示不同的内容时,通过诸如 Fetch 等 JavaScript API 来更新该单一文档的主体内容。它允许用户在无需从服务器加载全新页面的情况下使用网站。

实现 SPA 的一种流行方式是使用 React。React 使我们能够创建快速的应用程序,并且比 DOM 操作方法更易于简化用户界面的更新。

它具有以下几个优点:

  • 提升用户体验

    • SPA 加载一个单一的 HTML 文件,并在用户与之交互时动态更新内容。所有这些操作都无需完全重新加载页面。

    • SPA 可以轻松更新用户界面的状态,并根据应用程序上采取的操作向用户提供即时反馈。

  • 减轻服务器负载

    • 大部分工作由浏览器完成。这减轻了服务器的负载!
  • 更好的可扩展性

    • 现在大部分工作由浏览器完成。我们现在可以部署专门的服务器,专注于通过 API 提供数据服务。我们可以轻松地进行水平扩展。我们可以选择使用服务器或 Serverless 功能

    • SPA 可以托管在静态 CDN 上,如Netlify

随着像ViteCreate React App这样的工具链的加入,用于自动化现代 JavaScript 应用程序的设置,开发者们不再需要担心手动配置 Webpack。

实现 SPA 也存在一些缺点。其中一个主要问题是它依赖浏览器为我们加载所有的 JavaScript 和 HTML。这意味着在移动设备上以及对于网络速度较慢的用户,他们可能会在看到页面时遇到延迟。让我们来检查一下流程以解释这一点:

单页面应用程序流程

单页面应用程序 React 流程

用户最终看到 HTML 页面需要几个步骤。

首先,浏览器会获取 HTML。这个初始的 HTML 会是空白且不正确的。为什么呢?因为内容是来自 JavaScript 的。这意味着浏览器需要花费时间来获取 JavaScript、加载它并执行它。由于初始的 HTML 是错误的,网络爬虫和搜索引擎将无法在网站上找到相关内容并跳过它。

看一下下面的 GIF 图。在这里,在 Chrome 开发者工具中禁用了 JavaScript。没有 JavaScript,网站就无法加载。如果启用了 JavaScript 但网络连接缓慢,用户可能会在较长时间内看到一个空白页面。

单页面应用程序禁用 JavaScript 测试。

这是一个大问题。这导致了 Web 开发进入了预渲染时代

进入具有预渲染和 Hydration 的新世界

为什么预渲染很重要?

我们意识到可以提前生成 HTML。它可以从我们的服务器或在构建时生成,具体取决于所使用的方法。

预渲染可以通过两种方式完成

SSR(SSR)静态站点生成(SSG)

什么是 SSR?

在服务器上渲染 React 组件,然后将生成的 HTML 发送到浏览器。这可以提高 SEO 和初始加载时间。渲染过程在每个页面请求时发生

什么是静态站点生成(SSG)?

在构建时生成静态 HTML 页面。这些页面可以快速提供服务,而不需要服务器实时渲染它们。

这两种方法都是有用的!现在用户收到的 HTML 将是正确的。他们将看到一个有内容的页面,而不是像使用 Vite 或 Create React App 时看到的空白页面。

但有一个问题:用户收到的 HTML 不是交互式的。他们不能点击它或提交表单。我们如何为我们的应用程序添加交互性呢?通过正确的 Hydration🚰 🌊!

什么是 Hydration?

Hydration 是为我们的应用程序添加交互性的。它加载使我们的应用程序具有交互性的 JavaScript。

在 React 中,“Hydration”是 React 如何“附着”到已经在服务器环境中由 React 渲染的现有 HTML 上。在 Hydration 过程中,React 将尝试将事件监听器附加到现有标记上,并接管在客户端上渲染应用程序的工作。

让我们看看使用预渲染和 Hydration 的应用程序的流程是什么样的:
预渲染流程。

什么是 Reconciliation?

Reconciliation 是 React 确定响应数据或组件层次结构变化来更新用户界面(UI)的最有效方式的过程

Reconciliation 就是 React 弄清楚如何根据数据或组件层次结构的变化来更新用户界面(UI)。

当组件被渲染时,会创建一个虚拟 DOM(Virtual DOM)。如果状态或属性发生变化,那么会创建一个新的虚拟 DOM。然后,React 使用其差异算法将新的虚拟 DOM 与之前的虚拟 DOM 进行比较,以检查是否有变化。这就是Reconciliation

根据 Diff 的变化,React 不会更新整个用户界面(UI)。相反,它会选择哪些元素需要更新

预渲染和 Hydration 的实际应用

在预渲染和 Hydration 流程中,首先,用户会看到具有正确内容的 HTML。

然后 Hydration 开始发挥作用,加载 JavaScript 以使应用程序具有交互性。

让我们模拟一下如果 Hydration 过程由于网络连接缓慢而花费很长时间,或者如果用户禁用了 JavaScript 会发生什么情况。

这是一个 gif 动图,我 DevTool 中禁用了 JavaScript。使用Gatsby(一个具有 SSR 功能的静态站点生成框架)创建了我的应用

JavaScript 被禁用的测试。

即使没有 JavaScript,我们仍然可以在我的应用上看到内容。那是因为用户收到了预渲染的 HTML!可以看到,我们无法点击相关按钮。那是因为 JavaScript 没有加载,所以用户无法与之交互。

Hydration 的心智模型

在编译时的第一次渲染,生成所有静态的非个人内容,并在动态内容将出现的地方留下空位。然后,在 React 应用程序在用户设备上挂载后,第二次渲染会填入所有依赖于客户端状态的动态部分

总结:

预渲染和 Hydration 框架工作时的潜在错误及解决方法

第一次传递:我们看到预渲染的 HTML。它包含静态内容,但缺少动态内容。

第二次传递:JavaScript 开始加载并填入依赖于客户端状态的缺失动态部分。

当我们使用像 Next.js 这样的框架时,服务器会返回静态的预渲染 HTML,然后进行 Hydration 操作,加载 JavaScript。

但在处理动态数据和仅客户端属性时,我们必须小心。例如,看看这段代码:

动态数据错误

function HydrationErrors() {
  return (
    <>
      <h1>Hydration错误</h1>

      <div>
        <p>以毫秒为单位的今日日期是 {new Date().getTime()}</p>
      </div>
    </>
  );
}

在这里,服务器将生成一个带有以毫秒为单位的时间戳的 HTML。例如:1724869161034。Hydration 过程开始,然后客户端加载 HTML。时间已经过去,时间戳不同了,现在是172486193750!这种情况会导致以下错误:

服务器和客户端Hydration错误的文本内容不匹配。

这是因为getTime()函数会生成不同的时间戳。

这意味着服务器和客户端生成了不同的 HTML。网络选项卡向我们展示了服务器的响应。它与客户端加载的 HTML 不同。

以下是服务器的响应:

生成的不同服务器 HTML。

以下是客户端的响应:

生成的不同客户端 HTML。

解决错误的方法:

function HydrationErrors() {
  const [date, setDate] = useState<number>();

useEffect(() => {
setDate(new Date().getTime());
}, []);

return (
<>
<h1>Hydration错误</h1>

      <div>
        <p>以毫秒为单位的今日日期是 {date}</p>
      </div>
    </>

);
}

我们可以使用useEffect钩子。因为服务器和客户端渲染的 HTML 将包含一个空的date状态变量。

一旦组件挂载,useEffect就会激活并从状态变量中添加动态数据,或者我们可以使用suppressHydrationWarning标志并将其设置为true

<p suppressHydrationWarning="{true}">以毫秒为单位的今日日期是 {date}</p>

使用仅客户端属性导致的错误

我们不能使用windowlocalStorage。它们在服务器上不存在。看下面的例子:

 HydrationErrors() {
  return (
    <>
      <div>
        {typeof window!== "undefined" && <p>这个 p 标签将会显示</p>}
      </div>
    </>
  );
}

在这里,服务器返回带有一个空的<div>标签的 HTML,但客户端加载的 HTML 中包含了<p>标签。这就产生了一个Hydration 错误!

这就是会遇到的错误:

无法使用客户端属性的Hydration错误。

通过 DevTool 我们可以看到服务器的响应。它是一个空的<div>标签。

服务器的响应如下:

生成的不同服务器 HTML。

但客户端加载的 HTML 中写着“这个 p 标签将会显示”。

客户端的响应如下:

生成的不同客户端 HTML。

它与像 Gatsby.js、Next.js 和 Remix 这样的框架有什么关系?

我们所讨论的一切都是所有这些框架所关注的。

可以使用 Gatsby.js、Next.js 和 Remix 来实现静态站点生成和 SSR。它们专注于创建一个预渲染的 HTML,以便用户查看,然后启动 Hydration 操作来为应用程序添加交互性。

Gatsby.js、Next.js 和 Remix 并没有取代单页面应用程序的概念——它们为这个过程增添了内容。看看这个流程:

预渲染和单页面应用程序组合流程

它是在当前的单页面应用程序流程基础上进行添加!如果没有预渲染,那么这个过程将从粉色框开始的地方开始,此时的 HTML 是不完整的。

下一步

Next.js 首先通过页面路由实现了这些概念,它引入了像getServerSidePropsgetStaticPathsgetStaticProps这样的函数,以实现静态站点生成和 SSR。

这些实现有其优点和缺点

  1. 此策略仅在路由级别起作用,适用于树状结构最顶层的组件。我们无法在任何组件中都这样做。
  2. 每个上层框架都提出了自己的方法。Next.js 有一种方法,Gatsby 有另一种方法,Remix 还有另一种方法。它尚未标准化。
  3. 我们所有的 React 组件将始终在客户端进行 Hydration,即使它们没有必要这样做。

React 团队也注意到了这一点,并创建了一种称为React Server Components(RSC)的新范例。

为了实现 RSC,Vercel 团队创建了App Router。App Router 仍然使用预渲染和 Hydration 的概念,但它不再使用getStaticPropsgetStaticPathsgetServerSideProps

它使用 RSC 和其他 App Router 功能来实现更好的 Web 应用程序

更多好文,欢迎关注公众号Geek技术前线

参考 https://www.freecodecamp.org/news/what-are-pre-rendering-and-hydration-in-web-dev/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Geek技术前线

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值