Learning to Appreciate React Server Components In the beginning there was Marko Render-as-you-Fetch React Server Components Something Unexpected New Eyes Closing Thoughts

2025-06-10

学习欣赏 React 服务器组件

一开始是 Marko

即取即渲染

React 服务器组件

意想不到的事情

新眼睛

结束语

这是我的个人旅程,所以如果你来这里是为了寻找通用的“操作指南”,你在这里找不到。相反,如果你对我,一个 JavaScript 框架作者,如何努力看清眼前显而易见的事情感兴趣,那么你来对地方了。我面前确实有两部分,只是无法将它们串联起来。

我并没有忘记,我谈论的是一个尚未发布的功能,就像一段漫长的旅程,但对我来说确实如此。如果你不熟悉React 服务器组件,那么这篇文章就毫无意义了。你看,我们正处于 JavaScript 框架发展多年的激动人心的时刻,我们离它如此之近,几乎可以感受到它的魅力。

一开始是 Marko

现在你可能在想,这难道不是一篇关于 React Server 组件的文章吗?嘘……耐心点。我们马上就到。

你看,我每天工作12个小时。其中8个小时是我的专业工作,我是eBay Marko核心团队的开发人员。在和家人共度了一段非常宝贵的时间后,我的第二份工作开始了,我担任低调热门的新响应式框架Solid的核心维护者。

从技术角度来看,Marko 可以说是迄今为止最好的按需 JavaScript 服务器渲染框架解决方案。我认为它甚至还差得远,但这么说可能有点偏颇。但 Marko 的基准测试结果如此出色,而且这项技术是每个库都羡慕的(是的,甚至连 React 也不例外,但我们稍后会谈到这一点)。

如果您不熟悉 Marko,它是一个类似 Svelte 的编译型 JavaScript 框架,于 2012 年开始开发,并于 2014 年发布 1.0 版本。考虑到它支持渐进式(流式)服务器渲染,并且只将 JavaScript 加载到需要交互的客户端(后来演变为 Partial Hydration),那么 1.0 版本就堪称完美了。这两项特性是 2021 年 JavaScript 框架最令人垂涎​​的功能之一。

但这很有道理。Marko 从一开始就是为 eBay 打造的一款真正的大规模解决方案。它被积极推广,并在几年内占据了 eBay 网站的大部分业务。它取代了从一开始就作为全栈解决方案存在的 Java。而 React 在 Facebook 的采用路径则更加循序渐进。

Marko 在 2014 年提出了一个相当有趣的渐进式渲染系统。虽然它实际上只是一个使用该平台的示例,但奇怪的是,它在现代框架中却缺失了。正如 Marko 的作者 Patrick 在《异步片段:用 Marko 重新发现渐进式 HTML 渲染》一文中所描述的那样。

无需等待异步片段完成,而是将一个指定 ID 的占位符 HTML 元素写入输出流。乱序异步片段会<body>按照其完成的顺序在结束标记之前渲染。每个乱序异步片段都会渲染到一个隐藏<div>元素中。在乱序片段之后,<script>会立即渲染一个块,用相应乱序片段的 DOM 节点替换占位符 DOM 节点。当所有乱序异步片段都完成后,剩余的 HTML(例如</body></html>)将被刷新,响应结束。

自动占位符和插入功能(均属于流式标记的一部分,不在库代码范围内)非常强大。与 Marko 的部分数据加载 (Partial Hydration) 结合使用时,在某些情况下,在此之后无需进行额外的数据加载,因为页面中唯一的动态部分就是数据加载。所有这些都以高性能、非阻塞的方式实现。

即取即渲染

在阅读 React 的Suspense for Data Fetching文档之前,我从未听说过这种情况,但你最好相信我以前遇到过这种情况。

你不需要 Suspense 来实现这一点。你只需要让 fetch 设置状态并渲染你能渲染的内容,通常是一些加载状态。通常,父级会负责数据加载和加载状态,并协调页面的视图。

GraphQL 进一步扩展了这一功能,允许将片段与组件共置。从某种意义上说,您仍然可以将数据获取的控制权交给更高层级的组件,以便进行编排,但组件和页面仍然可以设置数据需求。然而,当代码拆分出现时,我们仍然会遇到一个问题。在导航时,您最终需要等待代码获取完毕后才能发出数据请求。

Facebook 已经用Relay解决了这个问题,它拥有严格的结构和工具,可以很好地并行化代码和数据获取。但你不能指望每个人都会用这个解决方案。

问题在于,简单的 JavaScript 代码无法拆分模块。你可以对未使用的代码进行树状结构优化 (treeshaking)。你可以延迟导入整个模块。但你无法只在不同时间引入所需的代码。一些打包工具正在研究自动执行此操作的可能性,但目前我们还没有实现。(尽管可以使用虚拟模块和一些打包工具的技巧来实现这一点)

所以简单的解决方案是自己进行拆分。最简单的答案不是延迟加载路由,而是为每个路由创建一个 HOC 包装器。假设路由器有一个 Suspense 边界,你可以这样做。

import { lazy } from "react";
const HomePage = lazy(() => import("./homepage"));

function HomePageData(props) {
  const [data, setData] = useState()
  useEffect(() => /* ... load the data and set the state */)
  return <HomePage data={data}  />
}
Enter fullscreen mode Exit fullscreen mode

我在 Solid 演示中坚持使用这种方法,以获得最快的加载时间。去年夏天的某个时候,我意识到这基本上只是个样板。如果我要为我们的新入门版创建一个类似 Next.js 的基于文件的路由系统,我希望解决这个问题。解决方案是在路由器中构建一个数据组件路由。

只需将它们的组件成对编写即可。homepage.js如果homepage.data.js存在第二个组件,库将自动连接它们,并为您处理所有代码拆分和并行加载,即使在嵌套路由中也是如此。数据组件将返回数据,而不是包装子组件。

从服务器和客户端的角度来看,这个提供常量isServer变量的库可以让任何打包器使用死代码,从而消除客户端仅使用服务器的代码。我可以让数据组件在服务器上使用 SQL 查询,并在客户端无缝地使用 API 调用。

React 服务器组件

2020年12月21日,React 服务器组件发布了预览版。我完全没想到它们的到来。我完全没想到,他们试图解决的主要问题已经有了解决方案。服务器上的 Suspense 完全可行,围绕代码拆分的并行数据获取也是如此。

能够识别哪些组件不需要包含在客户端包中固然很好,但需要手动操作。Marko 多年来一直能够通过其编译器自动检测这一点,但如果我们谈论的是交互式 SPA,我却没有发现这一点。尤其是当它使 React 的代码大小增加了 2 个 Preacts(JS 框架大小测量的标准单位)以上时。这里所做的任何事情都可以通过 API 轻松完成。如果你要设计一个支持 Web 和移动端的现代系统,为什么不使用 API 呢?

意想不到的事情

Adam Rackis 对 React 围绕并发模式的通信处理感到遗憾,这引发了关于 React 愿景的讨论。

最终,Dan Abramov,这位绅士,决定在一个不太活跃的论坛上(在周末)对Github 问题做出回应,讨论事情的进展。

这让我印象深刻:

比如,我们一开始根本就没有计划 Suspense,Suspense 是从流式服务器渲染器的探索中诞生的。

Suspense 是 2018 年初宣布的第一个现代功能,用于实现组件的延迟加载。什么?!这甚至不是它的初衷。

如果你仔细想想,将 Suspense 用于流式 SSR 就非常有意义了。服务器端 Suspense 听起来很像 Patrick 在 Marko 中对乱序渐进渲染的理解。

作为产品的消费者,我们倾向于按照接收顺序来理解每一条新信息。但我们被欺骗了吗?React 的功能设计真的反其道而行之吗?

作为一名框架作者,我可以告诉你,建立有状态原语似乎应该是第一步,但 Hooks 直到 2018 年底才出现。Hooks 似乎不是起点,而是从目标出发并回到可能的解决方案的结果。

如果将这一切放在 Facebook 重写的背景下,就会发现很明显,早在 2017 年甚至更早,团队就已经决定未来是混合的,而像服务器组件这样的东西就是最终目标。

新眼睛

意识到所有其他部分都开始各就各位。我之前看到的进展,实际上就像在看一部倒放的电影片段。

诚然,我之前也曾怀疑过这一点,但这表明他们很早就在服务器端处理了很多这种“即取即渲染”的场景。我猜想他们可能在某个时候也遇到过类似我数据组件的情况。

这周我碰巧在玩 Svelte Kit,注意到了它们的Endpoints功能。它提供了一种简单的单文件方式,可以通过创建.js文件来创建镜像文件路径的 API。我查看了一下,发现它的基本示例get与我的组件基本相同.data.js

那么,基于文件系统的路由需要什么才能注意到.server.js文件并将其保存为服务器上的数据组件,以及将它们转换为 API 端点,并自动生成对该 API 端点的调用作为客户端的数据组件呢?Vite比你想象的要简单得多。

结果是:你的代码始终在服务器上执行,即使在初始渲染之后也是如此。然而,它只是组件层级结构的一部分。这实际上回归了“单体应用”的单一同构体验。

如果数据是 JSX(或 HTML)编码而不是 JSON 数据,那么接下来会发生什么其实并不难理解。接收这些数据的客户端已经被 Suspense 边界包裹了。如果你能像初次渲染时一样,将视图流式传输到这些 Suspense 边界中,就能结束整个循环。

结束语

所以这个想法的演变其实很自然。很多平台都是基于 API 的,不需要“单体架构”,但这与主题无关。服务器组件实际上是我们在 Facebook Relay 中已经看到的并行数据加载和代码拆分理念的延伸。

我现在要去研究如何在所有地方实现它们吗?可能不会。Marko 已经展示了部分水合和积极代码消除的其他途径。在研究渲染方面之前,我将继续探索数据组件。但至少我觉得我更好地理解了我们是如何走到这一步的。

链接链接:https://dev.to/this-is-learning/learning-to-appreciate-react-server-components-49ka
PREV
Qwik——后现代框架
NEXT
如何使用 Git 命令清理本地存储库