写点什么

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:2610016

评论

发布
暂无评论

SnailSVN Pro for mac(SVN客户端)v1.10永久激活版

mac

苹果mac Windows软件 SnailSVN SVN客户端工具

商业模式画布的9大模块详细解读,一文弄懂产品经理必备技能!

彭宏豪95

创业 互联网 产品经理 商业模式 在线白板

Linux tar打包命令

芯动大师

ES6新特性(六)

阡陌r

JavaScript import ES6 export 模块化

Mac系统的防病毒软件推荐Antivirus Zap - Virus Scanner 最新中文版

胖墩儿不胖y

Mac软件 杀毒软件 mac系统维护软件

枚举

Mac修图必备软件Photoshop 2023破解版

iMac小白

photoshop下载 Photoshop2023 Photoshop2023 Mac

开发一个简单的管理系统,前端选择 Vue 还是 React?

互联网工科生

Vue React 管理系统

2023云栖大会议程&体验攻略

阿里云CloudImagine

云计算 云栖大会

文心一言 VS 讯飞星火 VS chatgpt (124)-- 算法导论10.5 5题

福大大架构师每日一题

福大大架构师每日一题

Downie 4 for Mac中文完美破解版 支持MacOS14

iMac小白

Downie 4 Mac版 Downie 4下载 Downie 4破解版

iStat Menus for Mac(系统活动监控器) v6.72 (1226)中文激活版

mac

苹果mac Windows软件 iStat Menus 系统监控工具

原来低代码开发如此简单

树上有只程序猿

软件开发 低代码 JNPF

DAPP 燃烧质押 TITAN 挖矿系统开发

l8l259l3365

鸿蒙OS应用开发初体验

巫山老妖

鸿蒙开发 鸿蒙系统

Acrobat Pro DC 2023 for mac中文完美破解版

iMac小白

Acrobat Pro DC 2023 Acrobat Pro DC下载 Acrobat Pro DC破解版 Acrobat Pro DC mac

Linux zip命令:压缩文件或目录

芯动大师

大家使用 Sealos 一键部署 Kubernetes 集群

米开朗基杨

Steinberg Cubase Pro 12 for mac激活版下载

iMac小白

Steinberg Cubase Pro Cubase Pro 12 Cubase Pro 下载 Cubase Pro 破解版

Sketch for Mac最新破解版下载 完美兼容M1

iMac小白

sketch Mac Sketch下载 Sketch 98 Sketch破解版

快手持续落地AIGC新应用场景 开启内测“AI小快”

Geek老T

AI 短视频 AIGC

第17期 | GPTSecurity周报

云起无垠

两种情况下 不能放弃云计算! | David Hansson

B Impact

DHorse改用fabric8的SDK与k8s集群交互

tiandizhiguai

OPPO Find N3,解码“新商务场景”

脑极体

OPPO

Dual band WiFi 6 power with IPQ4019 and QCN9024 chips - the wireless future of choice

wifi6-yiyi

IPQ4019

项目经理必备:6种有效的项目估算方法

爱吃小舅的鱼

项目经理 项目经理项目估算

国外怎么传大文件到国内,这款传输软件跨国企业必备

镭速

国外传输文件 跨国传输软件

问鼎之战 蓄势待发——鲲鹏应用创新大赛2023全国总决赛即将启幕!

Geek_2d6073

Linux环境变量及作用

芯动大师

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