我从“过时”的 React 开发中汲取经验教训

不少开发者时至今日仍然觉得 React 就是前端的现代标准。所以在切入正题之前,咱们先给 React 的真实水平“祛祛魅”。

其实我这篇文章,灵感来自 Alex Russell 在 Mastodon 上发表的帖子:

今天有人问我,能不能在不需要支持 IE 的新应用中使用 React。

我想了半天,也找不到一个非得这么干的理由……

React 的落后程度,着实令人有些惊讶。

Alex 在帖子中提到,React 缺少对 Web 组件的支持能力。而且这不是新问题了,React 多年来一直没能补齐这块短板。没错,开发团队总说“已在路线规划当中”。但截至本文撰稿时,他们仍然没在具体实现或者预计发布日期上做出明确承诺。

与此同时,几乎所有 React 的同类产品(就是那些可以用来替代 React 的框架或技术),都已经把 Web 组件支持落地并投入了生产。

而且 Web 组件只是其一,这份“应该做到/应该做得更好”的清单上还有密密麻麻的更多条目。下面我就再简要介绍几条。

给 React“祛祛魅”

React 因为较早投身于框架标准的制定而受益匪浅,甚至在很多人眼中成了标准本身。但它在敏捷性和适应性方面存在严重缺陷。自从 2013 年左右开始,React 做出的每一项决策几乎都在加重技术债——而其他比较年轻的同类框架则早已摆脱了这些束缚。

再次引用 Alex 的发言:

React 相当于一直在针对 08 年的限制设计 13 年的技术。现在已经是 2023 年了,React 也彻底跟创新断绝了关系。事实上,它可以说是当前在实现函数式前端编程方面最为迟钝的方案……

React 已然老去,可能大多数人都没意识到它在变老、或者说已经老到了什么程序。所以我用另外一种方式解释给大家听:

React 设计至今,泰勒·斯威夫特已经出了七张专辑,而且那时候约翰·梅尔还跟詹妮弗·安妮斯顿是一对儿呢。(这七张专辑,甚至没算 Taylor’s Version 这张重制专辑。)

所以如果屏幕前的你正好也在过去几年间错把 React 当成了整个前端世界,那你可能错过、甚至完全没意识到自己失去了什么。毕竟大家用 React 已经太久,错把不必要的问题当成了现实。

而且尽管现代前端技术一直发展极快,但我们的思维似乎反而更僵化了,没有意识到从很多方面来说,React 为王的时代早已过去。(更严格地讲,因为大多数组织面对的实际需求从来就跟 Facebook 不一样,所以 React 它就没「为过王」。)

过去十年间,浏览器在 JavaScript 和 CSS 方面的新功能采用量一直急剧增长。技术和用户期望已经推到了这个位置,现有工具生态系统在迭代和适应原有 React 成果方面的表现,绝对远远超出大家的想象。至少在前端开发领域,越新的往往还真就是越好的,而传统软件已然跟不上潮流。

用惯 React,你所失去(或者根本没感受过)的那些美好

我们真的用不着再盲目追求庞大的生态系统了,毕竟现在生态共享已经为成主流。

每当有“未经证实”的新框架出现在我们的项目开发流程当中,大家关心的第一个问题似乎永远是:它的生态系统大不大、强不强?

甚至还没开始读这篇文章,很多朋友脑袋里已经出现了这个问题。不用 React,转投其他框架的怀抱?它的生态够大吗?

其实这是件很诡异的事,为什么我们会对生态系统的规模如此痴迷?

当然了,我们都不希望自己的框架用着用着就消失了,至少不能几年间就失去了更新和维护。这个完全合理。另外,我们也不可能把全副身家都押在那些太新、或者未经证实的技术上。但无论是 Vue、Svelte、Preact、Solid 还是 Astro,明显都远远超过了这个阶段。它们都有良好的支持和维护,所以对生态规模的过度追捧肯定还有更深层次的原因。

那症结究竟出在什么地方?我整理了一套自己的理论:

我们已经被“驯化”了,习惯于专门给自己待定的框架建包。

有经验的朋友可能知道,这种习惯最初源自 jQuery,但 React 的大热则使其成为通行标准。

在 React 这边,任何一个模块、小部件或者是库等等(包括 carousel、map 或者 accordion 之类)但凡要想发挥作用,就必须得专门针对 React 进行构建——常规的 Web 或者 JavaScript 要素是不行的。React 在状态处理和组件生命周期的规则上,就强制要求任何非明确为其编写的包或者库都有可能无法工作。

React 告诉我们,一切事物都需要专门为某个框架进行构建。但这其实已经没什么必要了,或者说本来就不应该是这样。

没错,我们压根不该这么做。毕竟你 React 不是总宣称自己是“纯 JavaScript”框架吗?既然是纯 JavaScript,那就应该能跟一切纯 JavaScript 要素协同运作才对。

当然,其他前端框架也难免会有自己在状态、架构方面的一些规则和惯例,我们偶尔也会掉进它们挖的坑里。我承认,就算是 Svelte 或者 Vue 之类,也或多或少要做一点针对性的构建和调整。

但最大的问题是,这里我要明确强调这一点:

还没有其他哪种现代前端框架,会像 React 这样顽固地表现出跟平台间的不兼容。

如果大家正在使用其他现代工具和框架进行构建,那市面上可用的普通 JavaScript 包大概率已经能切实满足你的需求,而且这样的包可是以成千上万计。它们几乎不会导致渲染周期或者其他特定于框架的问题,而且都提供使用 Web 组件的选项。

也就是说,我们通常用不着为自己的项目定制专门的包或者库,因为需要使用的这些要素很可能已经跟平台兼容了。这样的开发过程才叫顺畅、才叫丝滑。

Preact Signals 就是个典型例子:虽然它是为 Preact 而构建的,但却能在任意框架中导入和使用,甚至连普通 JavaScript 也不例外。Web 组件也是,几乎跟一切除 React 以外的现代框架相兼容。

当框架有所欠缺的时候,平台往往也能出手补齐短板(比如说表单提交,这在 React 中一直是个痛点。但现在通过双向数字绑定加浏览器提供的约定,整个实际已经相当轻松。)

最糟糕的是,哪怕是需要做额外构建的情况,其实施难度也要远低于 React。(至少不需要把 useState 跟其他框架的版本比来比去。)

对于思想保守的开发者来说,在项目中使用新工具、新成果往往不是啥好事,他们会非常谨慎地尝试那些尚未经过全面验证的东西。但请大家务必牢记,新事物也有自己的优势,比方说技术债更少、直接放弃对陈旧浏览器的支持等。此外,新产品还能更自由地放飞新灵感、在更加现代的浏览器功能之上进一步迭代。

React hooks 其实有点过时了

Hooks 是 React 的最新发展成果,用以取代之前的类组件。

必须承认:hooks 代表着前端领域的巨大转变。它们彻底改变了我们在应用程序中的逻辑和状态组合方式。另外 hooks 也的确非常棒,几乎每个框架都围绕着类似 hooks 的模型来实现状态管理。

但 React hooks 这东西真的不新鲜了。实际上,React 的稳定 hooks 几乎跟我们家孩子一样大,而这小子再有几周就该上学前班了。

可以说 hooks 已经不算是什么竞争优势,甚至不再是什么功能亮点——其已经成为一种基准,成了我们最常规的开发方式。

其他框架不单也有自己的 hooks 实现 ,而且更重要的是:它们要么更快、要么更智能、要么更易于编写,甚至三点兼而有之。

Preact 的 Signals 相当出彩;Svelte 超级简单的跨组件状态共享机制 store 也很棒。Solid 也有 Signals,甚至 Vue 3 的 composition API 也受到了 hooks 的直接启发。而且相较于 React 的实现 ,它们都有自己的一些核心优势。

Hooks 模式值得表扬,React 也对它的全面普及起到了重要作用。但几乎每种其他框架都要做得更好、规则更少、而且不那么依赖样板。

如果大家不太熟悉 Signals 的概念,这里简单介绍一下:粗暴总结的话,我们可以把它看作 reactive State 的一种迭代演进;是种对 hooks 的更新,能以更好的默认项处理重新渲染。Signals 不再重新渲染整个组件,而只处理需要重新渲染的节点。

渲染早就不需要微观管理了

首先我得承认:我并不确定 useMemo 和 useCallback 之间到底有什么区别,也不知道什么时候该用、什么时候不该用。真的,哪怕我在写文章之前认真读了不少相关帖子,还是没太搞清楚。

还有另外一点:我仍然不清楚什么该用 useEffect 依赖数组,什么时候不该用,也不知道为什么要用。我感觉每写入一个 useEffect 调用,我都得花 15 分钟重构代码,让它符合 linter 格式。哪怕有 99%的时候它都能运行良好,我也不想被那 1%的几率拖入无底深渊。

我敢打赌,如果大家用过 React,那肯定也会跟我有类似的感受。也许你已经接受了这种模糊性和玄学意味,并且觉得现实就是这样。但我真心想要提醒大家:

在其他框架里,我们已经有很多年不用对渲染周期做这种微观管理了。

现在的框架已经足够聪明,完全可以自己搞定这些问题。而不需要你牵扯着它的手,一步步解释它们应该做什么。

现代框架们也都知道,如果没有必要,就别把宝贵的资源浪费在重新渲染上。它们很聪明,知道只需要更新值,而不是不断重新评估那些根本不需要的东西。

……当然,它们也并不完美,有时候也会犯错。但至少在知道要做什么、还有在默认情况下如何高性能地达成目标这件事上,它们做得普遍比 React 要好得多。

其他框架上也有需要优化的部分,但这种优化需求跟 React 相比简直就是小巫见大巫。

其他框架的 useEffect 版本用起来也更友好

当我们希望组件在进入 DOM 时做点什么,且/或希望它能根据其他数据或变量以动态方式重做某些计算时,几乎所有 其他框架都有比 React 上 useEffect 更好的办法。

关于这个问题,应该用不着我多费口舌了。毕竟在 React 社区之内,useEffect 也是出了名的危险,甚至很多老手建议彻底别用。总之请相信我,除了 React 以外,没有哪种其他前端框架会让人们如此害怕使用一项正常、有用的功能,也没有哪种框架会以如此迟钝的节奏处理这个致命问题。

真的没人会为了在安装组件时实现一点点功能,就去费力寻找第三方包——这纯属是没事找事。

扩展已不再是前端关注的重点

每当有比 React 更新的框架出现时,人们总是爱问:它的扩展性咋样?其实这个问题,如今也没啥必要了。

首先要强调一点:当初 React 诞生的那个时代,面对的现实问题跟现在不同。

在那个时代,大多数前端 UI 都是用原生 JavaScript 或者 jQuery(之类)构建的。而现在我们知道,这种应用构建方式确实无法很好地扩展到特定范围之外。

这是因为我们必须为每个要素、每个 DOM 节点都编写相应的选择器,还必须自己手动跟踪和同步状态,而这往往涉及混乱且极易出错的 DOM 写入和读取。更要命的是,这些操作的速度也很慢(因此才会出现虚拟 DOM,虽然它也已经过时很多年了)。

在那个时候,编写模块化代码几乎是不可能的,JS 文件经常会膨胀到几百甚至好几千行。如果有多个开发者在同一项目上工作,那他们经常会重写、重复甚至覆盖掉彼此的代码(部分原因是代码经常会进入共享的全局命名空间,因此有可能发生冲突)。你的应用越大、越复杂(比如像 Facebook 那样),那冲突问题就越严重。

所以一条铁律被深深刻进开发者的骨髓,这也成了前端“可扩展性”的基准:必须保证即使应用规模呈现出指数级增长,也仍然保有合理的可维护性。

总之,担心前端框架无法扩展是种跟 jQuery 一样古老的思维惯性,属于现代 Web 开发道路上已经过时的陈旧观念。

React 确实解决了很多问题,但它并不是现代工程学的奇迹,而只是想出了一种管理和共享状态的好办法。它让数据有了响应性,把复杂性抽象出来,并帮助开发者在不引发冲突、命名空间冲突或覆盖的前提下,得以共享相同的编程模式。

React 绝不是前端可扩展性方面最好、唯一甚至是最早的解决方案。相反,它只能说是同一范式下多种可行的方案版本之一。(也是最古老的方案之一。)

大家可能会问,你怎么就敢言之凿凿?因为有人运行了大量基准测试并公开了结果,把 React 的性能跟其他所有前端框架进行了大规模比较。(这里我就不贴链接了,毕竟资料在网上到处都是。)所有研究都证实,前端领域中几乎所有其他框架选项都比 React 表现更好,多数情况下甚至可以说是好得多。

这里,我指的是一般意义上的可扩展性,包括将复杂度控制在最低水平,而且不会随应用体量的提升而线性增长。当然,也有些框架在特定场景下的可扩展性更好或者更差,比如用 Markdown 文件构建静态 HTML、或者其他更加具体的任务方面。这个就要具体情况具体分析了。

服务器端渲染不再需要特殊对待

之前,我曾经错误地把服务器端渲染跟 React 服务器组件搞混了(但考虑到它令人困惑的命名约定,出点小偏差也在情理之中……对吧?)。

几年之前,React 几乎是唯一能实现服务器内容渲染的框架(主要通过 Next.js 实现)。当时,人们对 React 可以在服务器上作为 HTML 渲染的想法莫名兴奋,因为这全面颠覆了客户端单页应用(SPA)的通行标准。服务器端渲染带来了不可忽视的速度与 SEO 提升,所以在一段时间里,React 相较其他框架形成了领先优势。

但大家肯定也猜得到:最早的版本几乎不可能是最好的版本。

SvelteKit 默认在服务器端渲染,大家不用做任何额外操作,其中还提供对渲染模式的细粒度控制选项。

Resh(Deno 的前端框架)也是全服务器渲染的,只有特别指定的“孤岛”(island)才会在客户端渲染,其他一切均仅作为静态 HTML 发布。Fresh 还用到了 Preact(比 React 速度更快,有 Signals,外加性能更好、更符合直觉使用习惯的 useState 版本和响应式模型)。

Astro 也支持服务器渲染,允许开发者在服务器端渲染需要的任何组件。它对其他框架的组件也有很好的渲染效果,某些情况下甚至可以作为 Next 的性能升级选项。

SolidStart(Solid 的元框架)提供服务器渲染功能,Qwik 就是完全围绕这个中心构建而来。甚至 Ember 和 Angular 等比较陈旧的框架也有类似功能,这里就不一一列举了。

重点在于:以往,React 确实是少数几种能在服务器端渲染客户端视图框架组件的方案之一。但如今,服务器渲染早已成为桌面平台的主流。许多新兴框架不仅提供服务器端渲染选项,甚至还把它当成默认设置。

PHP 又回来了,朋友们。

双向数据绑定并不困难,效果也不错

我还想再强调一点:React 是 Facebook 开发出来的,为的是解决 Facebook 面对的独特问题。

React 最强烈的倾向性之一,就是认为数据应该只以一种方式(自上而下)流动。从这个角度,也能看出 Facebook 在 2010 年代早期所面对的现实问题,如何塑造了 React 的架构与基因。

有那么一段时间,人们甚至把单向数据流视为最佳实践。但现在,我们已经找到了解决双向数据绑定缺陷的解决方案,并意识到在多数情况下,双向数据绑定实际更加方便。

在 React 中处理表单是出了名的麻烦,因为用户的每一次键盘输入都对应两个步骤:从输入中获取值,之后设置状态来匹配这个值(这反过来又会对输入进行毫无必要的重新渲染,以此包含已经获取到的实际值,并跟 React 状态保持同步)。所以虽然大家在使用时可能察觉不到,但麻烦是真的麻烦。

Svelte、Vue 等许多其他框架就没这个问题,我们可以使用绑定状态的方式让它在两端自动更新。如果状态改变,那么 DOM 就会更新,而 DOM 的改变反过来也会触发状态的更新。

这样我们就不用像杂耍一样在各个步骤间反复横跳了。比如说,当我们想要捕捉某个文本框的值,就可以做双向数据绑定。之后当用户在字段中输入时,数据会自动更新,我们随时可以获取、无需借助任何额外步骤。如果在此期间还需要做点其他操作,比如设置一个值或者清除字段,那也是轻松加愉快。

双向数据绑定能够让数据和 DOM 保持同步,消除了不断确认二者同步的复杂步骤。

但双向数据绑定有没有短板?当然有,而且我发现最佳实践背后的种种僵化局限,几乎足以把好处消弭殆尽、甚至还不止。所以只要可以,还是尽量用单向数据流为好。

其实样式可以很简单

如果大家主要使用 React,那在前端组件中的样式处理上可能已然经历过两、三次迭代。

比如说,我们之前会直接把.css 文件导入 JSX 组件,或者使用 CSS 模块、样式组合和/或 Tailwind(可能使用 classnames 或者 tailwind-merge 包,再辅以额外的 Tailwind 插件)。而这些,还仅仅只是比较主流的办法。

Tailwind 也有自己的一团乱麻(我其实并不太喜欢它自带的前端框架;我觉得它违背了平台的本质,可能为了追求短期方便而损害长期利益)。但无论如何,这些样式解决方案的存在终究是好事,给了开发者更多选择。相比之下,React 那边打一开始就没提供过任何官方认可的样式选项。

很多朋友可能没发觉,样式在其他框架里早已不再是问题。

具体来讲,Vue 和 Svelte 都有自己的组件样式。它们都有组件级别的范围(Vue 是 opt-in,Svelte 则是 opt-out)。它们都跟原始 CSS 配合得不错,而且跟其他前端框架一样也都能跟 CSS 模块、Tailwind、Sass 或者其他你喜欢的方案良好兼容。

但最重要的是:任何 CSS 可能出现的问题(不管大家是否真的认为这是问题),都完全由内置的样式方案解决了。我们用不着面对一大堆包和配置,毕竟 scoped CSS 可以搞定你能想到的所有需求。

严格来讲,在看了这么多 CSS 不好的理由之后(其实不至于,但不太擅长 CSS 的开发者总喜欢这么说),我们对 CSS 的任何不满其实已经被 scoped styling 解决了。而且,除 React 之外的很多框架都已经 内置了这项功能。

新框架,已经没那么难学了

我猜测,很多如今熟悉了 React 的朋友应该还记得当初学习时的痛苦情景,而且觉得其他框架肯定也是这么难以上手。这样的顾虑阻碍了我们探索新事物的脚步——绝对很难,毕竟这可是第一次接触……

具体包括状态管理、props、嵌套、组件生命周期、hooks,还有如何编写 JSX 的诸多细节等等……即使是最狂热的 React 粉丝恐怕也得承认,对初学者来说这些都不是容易快速掌握的知识。(别嘴硬,当初大家明明都学得很费劲。)

但别担心,我给大家带来了个好消息:其实没有哪款工具像 React 那么难学,而且只要掌握了其中一种框架,往往也能快速上手其他框架方案。

我喜欢把接触新框架比喻成学习第二种乐器。第一次学音乐时,我们得了解关于乐理的各种知识,之后才能拿起乐器尝试让它出点动静。但在学习第二种乐器时,前面那些铺垫部分都可以直接跳过,所有概念已经了然于胸。你已经明白音乐是怎么回事,唯一要做的就是把原本的肌肉记忆稍微调整、转化成另外一种新的形式。

前端开发也差不多:每种框架都有组件,它们都跟 TypeScript 相兼容,也都有 props、children 和 reactive state 等概念。这些都是我们习以为常而且喜欢使用的技术成果,只是在不同框架上的具体实现各有区别。

说到这点:虽然 React 无疑助推了这些概念的落地,但给出的所谓“最佳实现”却相当笨拙、愚蠢。

伟大的事物都是通过迭代逐步完善出来的。所以在大多数情况下,后出现的前端框架自然在继承 React 核心思想的同时,迭代出了明确的比较优势。

也就是说,React 有点像一个落后于主版本的 git 分支。如果大家长时间只盯着 React、围着它打转,那很可能意识不到这一点。但现实在这里,前端开发已经整体迈进了一步。生态系统也接纳了这些想法,并在匹配之下让整个开发体验都上了个台阶。

我们现在不缺少性能更高、难度更低、学习曲线更友好的选项。或者说,大家连 React 都能啃下来,那其他框架根本不在话下。

如果 React 真的已经过时,那有什么靠谱的替代方案吗?请持续关注,下一篇我们会推荐一些“值得一试的其他选项”。

参与链接:

https://joshcollinsworth.com/blog/antiquated-react#part-2-things-you-forgot-or-never-knew-because-of-react

本文文字及图片出自 InfoQ

你也许感兴趣的:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注