面向 Astro 开发者的 RSC 指南
面向 Astro 开发者的 RSC 指南
2025年5月6日 随意赞助
好吧,在 Astro 中,你有两种东西:
- Astro Components:它们使用
.astro
扩展名。 它们专门在服务器上或构建期间执行。 换句话说,它们的代码永远不会发送到客户端。 因此,它们可以执行客户端代码无法执行的操作——从文件系统读取、访问内部服务,甚至从数据库读取。 但是,除了 HTML 或您自己的<script>
中原生存在的内容之外,它们无法执行交互操作。 Astro Components 可以渲染其他 Astro Components 或 Client Islands。 - Client Islands:为 React、Vue 等编写的组件。 这是你典型的前端内容。 在这里添加交互位很方便。 然后,这些 Client Islands 可以使用该框架自己的机制渲染同一框架的其他组件。 因此,正如您所期望的那样,React 组件可以渲染另一个 React 组件。 但是你不能从 Client Island 渲染 Astro Component。 那没有意义——到那时,Astro 已经运行了。
这是一个 PostPreview.astro
Astro Component 渲染一个 LikeButton
Island 的示例:
---
import { readFile } from 'fs/promises';
import { LikeButton } from './LikeButton';
const { slug } = Astro.props;
const title = await readFile(`./posts/${slug}/title.txt`, 'utf8');
---
<article>
<h1>{title}</h1>
<LikeButton client:load />
</article>
import { useState } from 'react';
export function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'} Like
</button>
);
}
请注意,Astro Components 和 Client Islands 本质上生活在两个不同的“世界”中,并且数据始终向下流动。 Astro Components 是所有预处理发生的地方; 它们将交互位“移交给” Client Islands。 现在让我们看看 React Server Components (RSC)。
在 RSC 中,相同的两个东西被称为 Server Components 和 Client Components。 这是您将上述 Astro Component 编写为 React Server Component 的方式:
import { readFile } from 'fs/promises';
import { LikeButton } from './LikeButton';
async function PostPreview({ slug }) {
const title = await readFile(`./posts/${slug}/title.txt`, 'utf8');
return (
<article>
<h1>{title}</h1>
<LikeButton />
</article>
);
}
'use client';
import { useState } from 'react';
export function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'} Like
</button>
);
}
这两种背后的心智模型非常相似! 如果你了解 Astro,你就已经掌握了 React Server Components 80% 的心智模型。(即使你认为 React Server Components 是一个糟糕的主意,Astro 也值得学习。) 让我们注意一下您可能注意到的几个语法差异:
- 与 Astro Components 不同,React Server Components 是常规 JavaScript 函数。 它们不是“单文件”的。 props 来自函数参数,而不是来自
Astro.props
,并且没有单独的“模板”。 - 在 Astro 中,Astro Components 和 Client Islands 之间的分离是通过将前者编写为
.astro
文件来实现的。 一旦你导入一个 Client Island,你就不再处于.astro
文件中,因此你就“离开”了 Astro 世界。 在 RSC 中,同样的目的通过'use client'
指令来实现。'use client'
指令标记了 Server 世界“结束”的地方——它是两个世界之间的一扇门。 - 在 Astro 中,有一些指令(例如
client:load
)允许您将 Islands 视为静态 HTML 或在客户端上可水合的 HTML。 React Server Components 不会将这种区别暴露给用户代码。 从 React 的角度来看,如果一个组件被编写为交互式的,那么删除这种交互性将是一个_错误_。 如果一个组件确实_不需要_交互性,只需从它那里删除'use client'
,然后从 Server 世界导入它_已经_会使其仅限于 Server 端。
最后一点很有意思。 在 Astro 中,不同的语法(.astro
文件与 Client Islands)在两个世界之间创建了鲜明且明显的视觉区别。 同一个组件不能同时充当 Astro Component 和 Client Island,具体取决于上下文——它们是具有不同语法的两个不同的事物。
但是在 RSC 中,“Astro”部分也“只是 React”。 因此,如果你有一个不使用任何客户端特定_或_服务器特定功能的组件,它可以扮演这两种角色。
考虑一个 <Markdown />
组件,它执行自己的解析。 由于它不使用任何客户端功能(没有 State)或任何服务器功能(没有读取 DB),因此可以在任何一侧导入它。 因此,如果你从 Server 世界导入它,它将像一个“Astro Component”一样行动,但如果你从 Client 世界导入它,它将像一个“Client Island”一样行动。 这不是什么新概念,这只是导入函数的工作方式!
在 RSC 中,从 Server 世界导入的东西将在 Server 世界中运行; 从 Client 世界导入的东西将在 Client 世界中运行; 并且在任何一个世界中都不支持的东西(例如 Client 上的 DB 或 Server 上的 useState
)会导致构建错误,因此你将被_迫_使用 'use client'
“切开一扇门”。
这既是祝福也是诅咒。
这是一个诅咒,因为它使得学习使用 RSC 相当不直观。 你一直担心“你身处哪个世界”。 需要练习才能接受它_并不重要_,因为你总是可以在本地进行推理。 你可以在需要它们的server端文件中使用 DB 等server端功能,在需要它们的client端文件中使用 State 等client端功能,并依靠构建时断言,如果出现问题会导致错误。 然后你查看模块堆栈跟踪,并决定在哪里为你的“islands” “切开一扇新门”。
这_确实_是一个诅咒,但它也是一种祝福。 通过在两端都拥抱 React,RSC 模型解决了你可能遇到的 Astro 的一些限制:
- 有时,你可能会编写一堆 Astro Components,然后意识到你需要将该 UI 移动到 Client Islands(同时调整语法),甚至复制它,因为一些动态 UI _也_想要驱动它们。 使用 RSC,你可以提取共享部分并从两端导入它们。 对于每个 UI 片段,仔细考虑“这部分主要将是动态的”或“这部分主要将是静态的”并不那么重要,因为你总是可以添加或删除
'use client'
和/或以很小的摩擦向上或向下移动它。 你确实决定在哪里“切开门”,但没有“来回转换”。 - 在 Astro 中,你_可以_在 Client Islands 内部嵌套 Astro Components,但如果这些组件包含_更多_ Client Islands,它们仍然会被你的框架(例如 React)视为单独的根。 这就是为什么_嵌套交互行为_不像在客户端应用程序中那样自然地组合,例如,React 或 Vue 上下文无法在 Astro islands 之间传递。 在 RSC 中,这不是问题——整个 UI 在底层是一个单一的 React 树。 你可以在一些 Server 子树之上有一个 Client 上下文提供程序,然后在下面任何地方有一堆读取该上下文的 Client 组件。 RSC 是 分形的 islands。
- Astro Components 最终只能生成 HTML。 这就是为什么在 Astro 站点上单击链接需要浏览器完全重新加载页面。 如果这对于你的用例来说看起来是可以接受的 UX,那就太好了! 你可以使用手动逻辑和 View Transitions 改进它,但从根本上说,页面的 HTML 确实_会被替换。 如果你想要一个类似 SPA 的导航,始终保持导航 Chrome 的状态,无论是任何 React 状态还是 DOM 状态(例如输入和滚动位置),那么 RSC 可以填补这个空白。 RSC 使用类似 JSON 的 格式来表示 React 树——它可以_转换为 HTML(对于首次绘制),但也会在导航时作为 JSON 重新获取。 换句话说,RSC 让你_思考_在 MPA 心智模型中——但它_感觉_像一个 SPA。
- 这也意味着,与 Astro 不同,RSC UI 的 Server 部分是_可就地刷新的_。 如果你确实运行一个服务器(而不仅仅是在构建期间运行 RSC,就像我为我的博客所做的那样),RSC 让你可以在任何时候“刷新”屏幕,让_新鲜的服务器 props 流入你已有的有状态的客户端树中_。 例如,如果一些 Astro Component 需要响应交互而刷新,你将不得不选择完整页面刷新或将逻辑移动到 Client Island 之间。 在 RSC 中,你可以只要求从服务器获取的新鲜 JSX 合并到树中。
在 Astro 中,基本输出格式是 HTML。 由于前端框架从根本上不操作 HTML 本身(它们操作一个有状态的 DOM,可以用 HTML 初始化),因此 Astro 遵循“一次性移交”模型。 这使得它在某种程度上更容易学习,但将服务器功能限制为“首次渲染”(到 HTML)所需的功能,并且主要让你自己处理交互位。 随着你使更多事物具有交互性,你可能会发现自己遇到了 Astro 模型的局限性,可能选择将更多逻辑移动到类似 SPA 的但孤立的 Islands 中。
在 RSC 中,基本输出格式是 React 树(可以转换为 HTML,但也可以作为 JSON(重新)获取)。 由于 RSC 在两端都使用 React,并且两个世界之间没有视觉区别,因此学习使用它更具挑战性。 好处是,一旦你掌握了移动边界的窍门,它们就会变得非常流畅,并解决了你必须将代码“移入 Astro”或“移回 Islands”的问题,因为某些东西最终变得比预期的更静态或更动态。 你还可以保留相同的“只是将数据映射到 UI”的心智模型,无论 UI 是只读的还是需要响应突变而重新获取。 Server 部分深入到树中——与_它们的_ Client 部分交织在一起。
并且因为两端都是 React,所以所有 React 功能都端到端集成:例如,Client 上的 <Suspense>
声明性加载状态 将“知道”等待异步数据(来自 Server)、JS 和 CSS(当 Client 加载它们时)、字体和图像(具有合理的超时),甚至触发 View Transitions(请参阅 此处)。 在 React 中,每个功能的都经过设计,因此 Server 和 Client 片段可以任意嵌套、组合和就地刷新。 这是一个单一的树。 缺点是,购买 RSC 意味着购买 React。 RSC _是_全栈 React。
最后,值得注意的是,Astro 是一个框架,但 RSC 本身是较低级别的——可以将其视为框架的构建块,或框架可以实现的标准。 目前 RSC 的两个官方支持的实现包括 Next.js App Router(一个框架)和 Parcel RSC(不是框架)。
就我个人而言,我认为 RSC 的开发者体验仍然有些粗糙,但我也认为你可能无论如何都想学习它。 它有一些有趣的想法。
此外,如果你从未使用过 Astro,请尝试一下! 如果 RSC 让你感到困难,Astro 可能会为相同的想法提供更温和的入门途径。 而且如果你只使用过客户端 React,Astro 可能会解决你从未意识到自己存在的一些问题。 随意赞助 在 Bluesky 上讨论 · 在 GitHub 上编辑 overreacted