作者 | IshanKBG
译者 | 刘雅梦
策划 | Tina
从我开始从事 Web 开发到现在已经 4 年多了,对我来说,React 是一个特别的“恶心”东西,它的一切给我的感觉都是..... 在某种程度上都不对劲,这是在我发现 Preact、Solid、Vue 及许多其他框架以及它们的模式是多么清晰之前的感觉。信号量(Signals)、SFC 和许多其他东西都是将它们区分为“React 替代品”的几个因素之一,而且是非常好的替代品,以至于我可能会把这篇文章归结为为什么你应该停止使用 React,也许我会这样做。就最近而言,但我所说的最近大概是指这 2-3 年吧?React 将重点放在了他们称之为“React 服务器组件”的东西上,乍一看,你可能会有以下反应:
“嗯,我想知道这与 Next.js 和 Remix 的服务器渲染解决方案有何不同。”
“..... 为什么 React 作为客户端 UI 渲染库要研究一些只在服务器上发生的事情?”
“它们现在增强了服务器渲染的工作方式了吗?那是什么?它是在现有服务器渲染之上的吗?”
“这真的是一个不合时宜的愚人节笑话吗?”
如果你也有这样的想法,并且头脑清醒,你会非常失望地听到 React 只是抛弃了一切让它变得体面的东西,只是采取了最糟糕的方式来创建“服务器组件”,并且从本质上讲,它重新发明了 PHP,但更简单。我是什么意思呢?要回答这个问题,它牵涉很深。今天,我读了一篇来自某个白痴的文章,内容是为什么你应该在所有项目中使用“服务器组件”,读完这篇文章后,我真正明白了它所针对的人群以及这项技术本身是多么愚蠢。如果你有兴趣阅读原文,这里有链接。但我想我们真的需要从一开始就深入了解这件事的本质,这应该可以很好地解释我对很多事情的厌恶原因的由来。
React、JSX 和
服务器组件简史
那是 2011 年,Angular.js 和 Backbone 在当时仍然相对新鲜,但 Facebook 已经在内部开发自己的替代方案以供自己使用了,他们可能没有预测到他们的小项目将如何影响未来的大部分 Web 开发的历史。不记得 React(如 JavaScript 库)和 React Native 哪个是黑客马拉松的内部成果了,但其中之一肯定是。React 最初是作为客户端响应式 UI 组件的“轻量级”方法提出的。当时确实是如此,很多使用 Angular.JS(不要与 Angular 混淆)的人确实说过并证实了 React 更快。但有一个问题,没有人真的想要编写嵌套函数调用,不,这看起来太可怕了,无法处理,除了一些人认为命令式函数调用“更好”,并像瘟疫一样反对 JSX。
JSX 是作为 JavaScript 语法的扩展而引入的,以帮助更好地编写 React 代码,此后它已经发展成为一种通用的语法规范,被 JavaScript 生态系统中的许多人使用,主要是 Solid 和 Preact,但 Vue 也提供了 JSX/TSX 作为编写 Vue 的选项(炫酷的一点是:根据尤雨溪(Evan You)本人的说法,Vue 特定于 JSX 的转换将进行更新,以适应 Vapor 模式的发布),但在内部,Svelte 也使用它通过 TypeScript 进行类型检查。我个人非常喜欢将 JSX 作为一种模板格式,它可以变成符号汤,但它与 JavaScript 本身的功能更相关,但我想从技术上讲,当你有一个编译器时,你可以允许更多。当然,“关注点分离”是一个问题,但实际上,想想看,如果这真的是一个大问题,你为什么认为我们正在使用的组件框架能将零碎的东西隔离成一个可重用的块呢?例如,SFC 文件与 JSX 类似,具有相同的目标,但实现方式却截然不同。好了,我们继续讲“服务器组件”。
“服务器组件”一词在 Web 开发中是一个宽泛的术语,但已经存在很长时间了。当想到它们时,一个有点预见性的想法是考虑 PHP 及其框架,甚至是 Ruby on Rails(尽管这是一个非常滑稽的、说明如何不做某事的大例子),但在 JavaScript 的背景下,人们可能会想到诸如 Pug、Liquid、EJS、Handlebars 之类的模板引擎,但一般来说,任何只在服务器上呈现为 HTML 然后发送到客户端的可重用模板或组件都可以算作服务器组件。那么,React 在这个范围内处于什么位置呢?在我的记忆中,React 总是可以提前将其组件呈现为静态 HTML。至少从 2013 年之后就可以了。你只需从“React-dom/Server”
包中调用renderToString()
方法(请注意:此方法调用已被弃用,取而代之的是其他 API),或调用包中的其他方法,将 HTML 渲染为流或其他任何内容。你可能会想,React 自己对服务器组件的看法又是什么呢?用一个词来回答这个问题:小丑。那么,它们能做什么客户端组件不能做的事情呢?简单地说,它们唯一要做的就是提供某种预渲染的模板,这些模板是“疏水性”的,不是说它们不接触水,而是说,与正常的服务器渲染组件相比,它们在默认情况下无法“水合”。RSC 还引入了一个重要的更改,这些组件可以是“异步”(async)的,而普通组件不能是异步的(直到use
钩子在稳定版本中发布),这造成了明显的分歧,导致了更多的摩擦,而不是减少摩擦。
Next.js 事件
不久前,Next.js 在 v13 中引入了对 RSC 的支持,老实说,从那时起它就已经开始走下坡路了。它引入了应用路由器(App Router),它附带了一种新的文件系统路由(File System Routing)方式。与直接在路由中依赖来流式传输 UI 不同,它还必须创建一个名为loading.js
的文件,这不仅使整个过程变得复杂,而且与 Next.js“借鉴”的其他解决方案相比,它完全是一个不必要的系统。虽然确实可以使用来流式传输 UI,但 Next.js 选择推广它们的新文件系统路由来实现相同的效果,尽管这种通过网络处理流式传输 UI 的方法有很多缺点。这意味着你需要这样做:
export default function Loading() {
return
;
}
就像这样简单:
export default function Route() {
return (
}>
Suspense>
);
}
另一件值得一提的事情是,Next.js 积极地允许并鼓励“组件级数据访问”,尽管这不仅是一种糟糕的做法,而且也是一种安全风险,至于为什么;稍后就会明白,为了解决这个问题,现就职于 Vercel 的前 Meta 开发人员 Sebastian Markbàge 写了一篇关于在 Next.js 应用程序上下文中处理安全性的博客文章。
服务器和客户端指令给开发人员带来了明显的精神开销,也带来了交互之间的摩擦,更简单、更高效是一回事,严重抄袭其他东西又是另一回事。最终,这确实感觉像是一个使用“use server”
指令的 PHP 克隆:
export function Bookmark(slug) { return (
{ "use server"; await sql`INSERT INTO Bookmarks (slug) VALUES (${slug})`; }} > button> ); }
Next.js 尽管标榜自己是“现代”且“友好”的,但它却喜欢引入与之截然相反的复杂且令人发指的做法,包括上面提到的安全性等。Tom Sherman 的一条推文指出,Next.js RSC 如何让你轻松地将秘密泄露给客户端,而不是像你期望的那样将它们留在服务器上;再次说明;糟糕的设计会导致糟糕的做法。
不好意思,我要扯个不相关的话题了。在过去的几个月,甚至几年里,Next.js 一直专注于快速发布越来越多的破坏性变更,而不是随着时间的推移而放慢速度进行更多的质量变更和改进,框架的缺陷已经越来越多,总体质量也急剧下降,以前 HMR 需要几百毫秒就能完成的事情,现在却需要 1-2 秒才能完成。Next.js 似乎也在某种程度上与 JavaScript 社区背道而驰,试图分裂并做自己的事情,除了“仅仅因为”之外没有其他原因,而不像其他工具那样最终握手言和并使用越来越多的共享工具,一个流行的例子是 Remix 和 Angular 现在在内部使用 Vite,另一个例子是 Solid 将信号量(Signals)正确地引入到了 JavaScript 世界,还有很多例子,包括 Vue 开发商之一的 UnoCSS。
另一件应该考虑的事情是,Turbopack 的存在,我唯一的问题是:为什么?
Turbopack 不仅通过误导性的基准测试进行了大量的虚假宣传,而且它或多或少也未能实现“Rusty Webpack 实施”的目标,社区已经有了很多很棒的、非常好的互连工具,我们对当前这一代构建系统的改进已经达到了一个顶点,很多旧的构建系统都有非常好的、几乎可以直接替换的替代品,比如:
Parcel->Vite(不是真的,但 Vite 是除了 Parcel 2 之外最接近 Parcel 1.x 的继任者)
Rollup->Rolldown(Rollup 的 Rusty 重写)
Webpack->Rspack(Webpack 的 Rusty 重写)
https://twitter.com/youyuxi/status/1587279357885657089
看看尤雨溪(Evan You)的基准测试,它揭穿了 Vercel 的这些说法。
那,Next.js 中的缓存难道也不像其他宣传的功能那样好吗?
与通常的普遍看法相反,事实并非如此;相反,它远远超出了应有的水平。默认情况下,Next.js 会缓存所有内容,不缓存意味着你需要明确选择不缓存的内容,这在很多情况下很容易让自己搬起石头砸自己的脚,如果我真的想让自己自食其果,为什么不使用 C++ 呢?当你打算绕过单个数据提取的缓存时,将cache
选项设置为no-store
即可:
fetch(`https://...`, { cache: 'no-store' })
现在,为了在路由级别绕过缓存,请深入研究路由段配置选项。你可以这样处理:
export const dynamic = 'force-dynamic'
然而,还有另一种选项,使用名为 unstable_noStore 的不稳定 API 来选择退出组件级别的缓存。它们建议大家使用这种方式,我明白你的意思。有时,你只需要获取动态数据,而不必担心缓存。这些选项似乎让事情变得比必要的更复杂,不是吗?如果你不小心,很容易向用户提供过时的数据,甚至错误地共享私人数据,因为这些数据是共享且持久的。Flavio 的这条推文解释了为什么它如此糟糕,另一件要注意的事情是,有一个 GitHub 讨论会让我非常担心为什么会这样;老实说,这是一个可悲的现实。https://twitter.com/flaviocopes/status/1736317822609887362
Remix 加载器和数据提取
大约三年前,一个名为 Remix 的框架开始受到热捧,这是理所当然的,原因有很多,它已经存在了很长时间,但源代码是封闭的,它们在那个时候开始开源,并有一些非常好的明确的品牌与它们的目标一致;利用现有的 Web 本身来推动它的发展。自然而然地,它们引入了基于 Web 标准的解决方案,包括内置组件和 React Router 的重新导出。Remix 的众多优点之一是数据加载器,这是一种加载服务器数据而不会泄漏到客户端的方法,就像 React Hook 一样。这是一种无需担心关注点分离即可加载数据的巧妙方法。以下是如何在 Remix 中加载数据。
export async function loader() {
const user = await getUser();
// 并行加载数据而无需创建瀑布流
const [notifications, comments, posts] = await Promise.all([
getNotifications(user.id),
getComments(user.id),
getPosts(user.id)
]);
return json({
notifications,
comments,
posts
});
}
export async function clientLoader({ request, params, serverLoader }) {
const [serverData, clientData] = await Promise.all([
serverLoader(),
getDataFromClient()
]);
return {
...serverData,
...clientData,
};
}
export default function Route() {
const data = useLoaderData
(); // 渲染你的数据 // ... }
这不仅更容易理解,而且符合人体工程学,并且还可以确保你不会轻易向客户端泄漏任何东西。你可以在这里阅读更多关于 Remix 是如何进行数据提取的信息。Remix 始终能使你的数据与 UI 保持同步。此外,你可以简单地使用名为 Data Actions 的 API,而无需将服务器端代码放在组件中来计算表单数据,它既高度安全,又符合人体工程学。
export async function action({ request }) { const body = await request.formData(); const bookmark = await createBookmark(body); return redirect(`/bookmark/${bookmark.id}`); } export function CreateBookmark() { const actionData = useActionData (); return (
Name:{" "} label> p > {actionData?.errors.name ? (
{actionData.errors.name} p > ) : null}
Description:
label> p > {actionData?.errors.description ? (
{actionData.errors.description} p > ) : null}
Create button> p > Form> ); }
Remix 的数据建模方法和为此提供的 API 比 Next.js 在 RSC 环境下的数据建模更符合人体工程学,也更简洁。
常见误解与狂热主义
“React 服务器组件的打包成本为零。”
在 Vercel Glazers 中普遍存在的一个误解是,它们认为通过使用服务器组件可以获得免费的性能,但事实并非如此,事实恰恰相反。RSC 在服务器上渲染,仍然需要通过网络发送,并带有自己的打包成本,就 Next.js 而言,它通常保持相同或略有减小的打包大小,而不是像宣传的那样减小或为零;另一个是运行时性能,如果你在 RSC 中加入一个客户端指令,RSC 就会“水合”,这就违背了 RSC 的初衷,即拥有完全只在服务器端渲染的 UI 位。RSC 首次使用服务器指令演示的流行表单示例实际上确实会将动态行为水合并补充到客户端 DOM 上。
Redwood.js 怎么样?
一样的垃圾;只是领域不同。Redwood 逐字复刻了 Next.js RSC;在不同的领域,逐条复制,毫无变化。这是一个可以理解的安全选择,但无疑令人失望。
那 Vercel 的设计呢?你为什么这么讨厌它?
就像我说的,它引入了更多的摩擦,并与 UX 和 DX 发生了冲突,这就是我讨厌它的原因,当我在下一节中谈论其他框架如何更好地处理它时,这将会开始变得更加清晰。
仅仅因为 Vercel 非常注重供应商锁定并从中赚钱,并且曾经为服务器渲染领域带来了复兴,并不意味着它们的所有决定都是好的或有效的,它们和大多数其他公司一样是由金钱驱动的,而不是将关注点放在打造出好的产品上,这已经反映在它们过去几年的行动中。
为什么大多数框架
能更好地处理这个问题
你可能认为只有 Remix 是唯一一个能更好地完成 RSC 某项工作的框架,但它并不是唯一一个这样做的框架,每个框架及其“元框架”都为这一问题提供了一个非常好的替代解决方案。在我看来,Qwik 做得就很好,因为它的最终目标是将服务器渲染作为第一方的事情。`@builder.io/qwik-city` 包导出了 2 个有助于解决这个问题的东西:
routeLoader$
routeAction$
乍一看,它们似乎是天造地设的一对,但那是因为它们确实如此,我必须强调这一点。例如,这将产生与 RSC 相同的效果,但更简洁:
可以通过以下方式将数据写入到服务器中:
export const useAddUser = routeAction$(async (data, requestEvent) => {
const userID = await db.users.add({
firstName: data.firstName,
lastName: data.lastName,
});
return {
success: true,
userID,
};
});
export default component$(() => {
const action = useAddUser();
return (
<>
"firstName" />
"lastName" />
"submit">Add user
{action.value?.success && (
User {action.value.userID} added successfully
)}
);
});
你也可以像这样加载数据:
export const useProductDetails = routeLoader$(async (requestEvent) => { const res = await fetch(`https://.../products/${requestEvent.params.productId}`); const product = await res.json(); return product as Product; }); export default component$(() => { const signal = useProductDetails(); return
Product name: {signal.value.product.name} p >; });
Qwik 的数据写入和数据提取方法不仅符合人体工程学,而且泄漏和引起安全问题的可能性要小得多,并且还与自动类型推断相结合,以实现更好的 DX,非常无缝;非常干净。
令人遗憾但可以预见的结论
总体而言,Next.js 对 RSC 的不健康痴迷是一种“改善用户体验”的营销策略,具有讽刺意味,因为它最终损害了用户体验,而且当其他方法已被证明可以实现相同的最终结果,可以获得更好的收益时,它也会无缘无故地让开发人员变得更加困难。最重要的是,从长远来看,Next.js 对 RSC 的实现使得很难证明其合理性,我非常期待 Remix 在来年的做法,一如既往,在交付方面,我对 Remix 团队充满信心。最后,向所有做得好、做得对的主要框架致敬:Remix、Qwik,甚至 SvelteKit。然而,这并不是说 RSC 是一个完全坏的主意,它们只是有自己的一套用例而已,例如:
更好的组件和 DX,如类型推断
不再需要使用 React Contexts 进行痛苦的工作。
老实说,如果 RSC 能实现的更好,比如像 Qwik 那样,我会喜欢 RSC。最终,RSC 的好坏将取决于特定于框架的实现,为此,我将依靠 Remix,并以此为基础,我暂时退出。
https://ishankbg.dev/archive/react-server-components-a-bad-idea/
声明:本文为 InfoQ 翻译,未经许可禁止转载。
热门跟贴