写点什么

React 服务器组件:一个坏主意?

  • 2025-01-26
    北京
  • 本文字数:7473 字

    阅读完需:约 25 分钟

React 服务器组件:一个坏主意?

从我开始从事 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)方式。与直接在路由中依赖 <Suspense/> 来流式传输 UI 不同,它还必须创建一个名为 loading.js 的文件,这不仅使整个过程变得复杂,而且与 Next.js“借鉴”的其他解决方案相比,它完全是一个不必要的系统。虽然确实可以使用 <Suspense/> 来流式传输 UI,但 Next.js 选择推广它们的新文件系统路由来实现相同的效果,尽管这种通过网络处理流式传输 UI 的方法有很多缺点。这意味着你需要这样做:


export default function Loading() {    return <LoadingSkeleton />;}
复制代码


就像这样简单:


export default function Route() {    return (        <Suspense fallback={<LoadingSkeleton />}>            <UI />        </Suspense>    );}
复制代码


另一件值得一提的事情是,Next.js 积极地允许并鼓励“组件级数据访问”,尽管这不仅是一种糟糕的做法,而且也是一种安全风险,至于为什么;稍后就会明白,为了解决这个问题,现就职于 Vercel 的前 Meta 开发人员 Sebastian Markbàge 写了一篇关于在 Next.js 应用程序上下文中处理安全性的博客文章。


服务器和客户端指令给开发人员带来了明显的精神开销,也带来了交互之间的摩擦,更简单、更高效是一回事,严重抄袭其他东西又是另一回事。最终,这确实感觉像是一个使用 “use server” 指令的 PHP 克隆:


export function Bookmark(slug) {    return (        <button            formAction={async () => {                "use server";                await sql`INSERT INTO Bookmarks (slug) VALUES (${slug})`;            }}        >            <BookmarkIcon />        </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<typeof loader>(); // 渲染你的数据 // ...}
复制代码


这不仅更容易理解,而且符合人体工程学,并且还可以确保你不会轻易向客户端泄漏任何东西。你可以在这里阅读更多关于 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<typeof action>();

return ( <Form method="post" action="/bookmarks/new"> <p> <label> Name:{" "} <input name="name" type="text" defaultValue={actionData?.values.name} /> </label> </p > {actionData?.errors.name ? ( <p style={{ color: "red" }}> {actionData.errors.name} </p > ) : null}

<p> <label> Description: <br /> <textarea name="description" defaultValue={actionData?.values.description} /> </label> </p > {actionData?.errors.description ? ( <p style={{ color: "red" }}> {actionData.errors.description} </p > ) : null}

<p> <button type="submit">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 ( <> <Form action={action}> <input name="firstName" /> <input name="lastName" /> <button type="submit">Add user</button> </Form> {action.value?.success && ( <p>User {action.value.userID} added successfully</p > )} </> );});
复制代码


你也可以像这样加载数据:


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 <p>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 翻译,未经许可禁止转载。


今日好文推荐


刚刚过去的一年 GitHub 刷星大爆发?!450 万假 Star,项目风光仅撑 2 个月!


C++用了11年,仅17位贡献者代码提交超过10次,迁移到Rust后,再也不想回去了


软件架构50年:大模型是否开启新的抽象层次?ACM 院士、UML创始人谈软件架构演变


微软全新原生 Copilot 应用被指是 Edge 套壳:从 PWA 转向“原生”,内存占用却飙升至 1GB


2025-01-26 11:2610972

评论

发布
暂无评论

WordPress 主题和插件

海拥(haiyong.site)

WordPress 5月月更

PingCAP 宣布 TiDB Cloud 正式商用,助力全球企业在云上构建新一代云原生应用

Geek_2d6073

快慢缓急总相宜|ONES 人物

万事ONES

SaaS到底是什么?如何做?

小炮

SaaS

程序员转型产品经理:懂技术或许是把双刃剑!

博文视点Broadview

uni-app技术分享| uni-app转小程序-实时消息

anyRTC开发者

小程序 uni-app 音视频 实时消息 呼叫邀请

队列同步器AQS

急需上岸的小谢

5月月更

网站开发进阶(五十五)CSS padding、margin 属性

No Silver Bullet

5月月更 padding magin

下个十年高性能 JSON 库来了:fastjson2!

王磊

Java

招商蛇口重塑客户经营新思路,推动多业态融合升级

科技热闻

JavaScript数据类型

源字节1号

软件开发 前端开发 后端开发 小程序开发

Go 学习笔记——函数篇一

为自己带盐

Go 5月月更

C语言_字符串与指针的练习

DS小龙哥

5月月更

Docker下的OpenResty三部曲之一:极速体验

程序员欣宸

Docker 5月月更 openrestry

ironSource 推出 Luna Views,通过定制化数据面板呈现多渠道广告效果

Geek_2d6073

教你用 ECharts 轻松做一个Flappy Bird小游戏

华为云开发者联盟

图表 eCharts 图表库 Flappy Bird 小游戏

为什么前端不能没有监控系统?

杨成功

大前端 构架 5月月更

李东山——如何让OpenHarmony支持低功耗蓝牙芯片GR551x

OpenHarmony开发者

OpenHarmony 低功耗蓝牙芯片

“可严可仁”的考勤系统,让数字化不漏掉人性化

明道云

解构HE2E中的Kubernetes技术应用

华为云开发者联盟

Docker Kubernetes DevOps HE2E CCE部署

C语言-strlen和sizeof强化习题练习- I

芒果酱

c++ C语言 5月月更

一文,教你打造员工生命周期解决方案

Authing

单点登录 零信任 数据泄露 B2E 元气森林

网站开发进阶(五十玖)css实现背景透明,文字不透明

No Silver Bullet

CSS 5月月更

浅述容器和容器镜像的区别

汪子熙

Docker 容器 容器镜像 虚拟化技术 5月月更

智能运维应用之道,告别企业数字化转型危机

云智慧AIOps社区

大数据 监控 数字化转型 智能运维 自动化运维

如何使用 Authing 单点登录,集成 Discourse 论坛?

Authing

低代码 单点登录 Idaas 应用集成方案 Discourse

作业帮在线业务 Kubernetes Serverless 虚拟节点大规模应用实践

阿里巴巴云原生

阿里云 云原生 客户案例 作业帮 Kubernetes Serverless

Java遇上SPL:架构优势和开发效率,一个不放过

华为云开发者联盟

Java stream 应用架构 SPL 结构化数据处理

WorkPlus统一门户:企业信息互通,实现业务协作

BeeWorks

druid源码学习二-对锁的理解

Nick

java 并发 lock锁

2022年记一次慢查询优化指南,MySQL 优化学习第9天

梦想橡皮擦

5月月更

React 服务器组件:一个坏主意?_架构/框架_IshanKBG_InfoQ精选文章