工程师 Jack Franklin 过去每天都使用 React,直到后来他加入谷歌从事 Chrome DevTools 工作,工作重点变成了引入 Web Components,他发现自己离开 React 之后好像也没有想象中的那样怀念 React。本文是他关于 React 和 Web Components 的一些实践与思考。
两年多前,我从伦敦一家初创公司离职,在那里,我负责开发基于 React 的大型电子商务前端。离职后,我加入了谷歌,从事 Chrome DevTools 的工作。我最初的工作重点是引入 Web Components,这是一个新的测试基本构建块,用于开发新的 DevTools 特性和用户界面。由于最近 Recorder 面板和其他面板的发布,现在,DevTools 中的很大一部分几乎都是 Web Components。
我曾经预计,当我离开以 React 为中心的角色后,会发觉转型很困难,并会怀念 React 所提供的东西。结果,我发现转型要比预期容易得多,并且我真的很享受更接近平台原语的工作,对我所编写的软件保持了更多的控制,在本文中,我想分享一下这个原因。
首先,以防引发歧义和争论,我想先清楚地阐明本文不是什么:
本文并非呼吁每个人都立刻抛弃 React,转而使用 Web Components。
本文并非宣告 React “死亡”的博文,也并非所有项目的错误选择。
本文并非宣称 Web Components 是所有项目的最佳解决方案。
本文亦并非宣称所有的 Web 框架都是没用的或者很糟糕的选项。
本文并非博文暗示,因为这种方法适用于我和我在谷歌的团队,因此对于你来说也是一样。所有的项目都不尽相同,而且几乎可以肯定的是,Chrome DevTools 对于你的项目有着不同的需求和约束。那也无所谓。本文应该被解读为一个人从每天使用 React 到不去触碰它的思考,以及这样做的经验。我之所以写下这篇博文,是因为我惊喜地发现,我是如此享受与 Web 平台更紧密的合作。
虽然我会用 “React” 作为我的比较,但你可以合理地将其替换为任何大型现代框架。
使用平台
近年来,“使用平台”已经成为一个有点被过度使用和滥用的短语,但是它的核心原理却让我产生了共鸣:我们能不能利用浏览器和 JavaScript 中的 API,在无需第三方依赖项的情况下,为我们的用户开发特性?
注意:这里的答案并不总是“是”!我还是想把更多的特性添加到浏览器中。但是,相对于十年前,这个原生特性的范围有了很大的拓展。一个典型的例子就是构建表单:这曾经是一个使用 React 的合理理由,因为在这里,浏览器只提供了原始的特性。快进几年后,在最近的一个项目里,我可以用 100% 的原生特性来构建具有坚实用户体验的表单:
我使用 HTML 验证属性来强制执行必填字段(还可以使用基于模式的验证做得更多)。
我使用 FormData API 从表单中读取数值,而不是在状态中跟踪它们的数值(我不需要这样做,因为验证是由浏览器完成的)。
如果我愿意,我甚至可以使用约束验证 API 来定制错误信息——直到几天前我才知道这个 API 的存在!这是不是比使用 npm 的一个库来为我打包这一切更费事?有可能!但是我可以通过多写一些代码来达到相同的效果,而不会因为额外的依赖项而影响到我的应用。
保持控制
当我刚开始使用 React 时,我最担心的就是如何适应自定义元素,但是我真的很喜欢用它们工作。
自定义元素可能会让你更容易写出更多的代码,但是,如果你以前用过任何一个流行的组件库,那么你就可以创建出一些令人惊讶的、熟悉的东西,但是有一个关键的区别:你不会放弃控制。
React 不允许你自行决定如何以及何时将你的组件渲染到页面上。你使用它的结构来编写代码,它决定何时渲染。10 次中有 9 次——甚至 100 次中有 99 次或更多——这完全符合你的预期。但是 Web 平台并非十全十美,我猜想大多数 React 开发者都会遇到过这样的情况:你很渴望能够调整自己的组件的渲染方式。
如何放弃对渲染过程的控制,将会导致混乱,正如 Gary Bernhardt 所发的这条推文一样:
为什么这段代码:
console.log(mark ${Math.random()})
alert(mark ${Math.random()})
打印一条日志,但是显示两个警报?因为React.StrictMode
隐藏了一条日志来“帮助”我为并发模式做准备。React 很好,但对于 99.9% 的应用来说,并发模式感觉就是个错误。这种行为现在已经在 React v18 中有所改变了,但是 React 不得不抑制额外的console.log
调用,这是它渲染我的应用的结果,这一事实让我感到惊讶;正是这种在我自己的应用中缺乏控制的情况,已经让我感到非常警惕。
在软件开发中,这被称为控制反转(Inversion of Control)。当你使用像 React 这样的框架时,你的代码将不再直接控制组件(或函数)何时被调用。你的组件并不直接告诉 React 何时重新渲染它们,而是由 React 决定。你的组件已经把控制让给了 React。
我们的自定义元素解决方案不会有这种控制的倒置;我们通过明确地调用一个函数(在 lit-html 的例子中,它是一个名为 html
的带标签模板源文本)来控制每一次渲染。
没有使用 React 这种框架的不利之处在于,你必须考虑重新创建那些原本内置的部分,比如,确保我们批量渲染的基本调度器,或者使这些组件更容易测试的测试助手库。在这种情况下,你必须仔细考虑你的选择:如果我们避免使用 React,但最终重新实现了它提供的大部分内容,那么我们使用框架会可能会更好。在我们的案例中,我们依然认为这样的决策是合理的,因为我们不必重新构建一个具有 React 的所有复杂性的调度器:我们可以构建一个小型的、独立的实现,只实现我们所需要的东西。
在构建了我们的基本调度器之后,我们可以清楚地知道每个组件的渲染原因和时间,并且当我们需要偏离标准路径时,我们就能够实现。这种感觉很有价值:我所开发的每个软件项目中,都有至少一种组件需要做一些不同的事情来处理奇怪的边缘情况。
选择可以轻松替换的依赖项
自定义元素缺乏的一个领域是某种形式的 HTML 模板解决方案,它提供了高效的 HTML 重渲染。我肯定会建议使用一个库来解决这个问题,而我们选择了 lit-html。lit-html 的魅力在于,它只是我们解决方案中的一小部分。我们本可以选择 Lit,一个基于自定义元素形成的功能更全面的组件库,但是这样做,会导致我们增加依赖项,同时也会失去一定的控制(重申我在这篇博文前面提出的观点:这不是对 Lit 的批评,对很多人来说 Lit 是正确的选择!)。
Lit-html 可以确保我们的 HTML 被有效地渲染,并且提供了一套很好的指令,让我们可以轻松地完成常见的任务,如有条件地应用类。虽然没有 JSX 那么完美,但是也非常的接近。
最重要的是什么?它是一个非常小的依赖项(3.3kB gzipped),更重要的是,如果我们需要的话,可以很容易地被替换。这听起来很消极,甚至很悲观,但当我们采用一个新的依赖项时,我们要问的一个主要问题是:“如果它消失了会怎样呢”?
假定 React 已经消失(这并不是说我认为它会消失)。我们处理这个问题的代价是什么?我们有几个选择:
维护 React 的复刻(fork),无论我们目前使用的是哪个版本。
将我们所有的组件从 React 迁移到其他地方。这两个选项对我都没有吸引力;维护一个库意味着我们要么什么都不做,要么错过改进和/或安全修复;而迁移我们所有的组件,那就是一项浩大的工程了。我确信,一旦出现这样的情况,React 的复刻将会如雨后春笋般涌现出来,但是不管怎么说,它都需要很多的努力,经过很多的波折,才能使事情变得更健壮。迁移我们所有的组件,花费会非常大,而且对最终用户来说,也没有什么实际的好处,这对于企业和领导层而言是非常困难的。我们还必须学习一种新的框架(即使它与 React 相似),并且在这个框架中增加我们的专业知识。
这与自定义元素和 Lit-html 形成了鲜明对比。我们可以有很好的信心,自定义元素不会突然消失;它被嵌入到浏览器中,并且向后兼容是 Web 平台的一个核心原则。
如果你在想自定义元素 v0 被移除而改用 v1,请记住 v0 是 Chrome 浏览器的特定实验性规范,而 v1 是一个跨平台的标准化规范。v0 的目的是收集开发者的反馈,以便为未来的标准化规范提供参考。要是 lit-html 从互联网上消失了呢?我们有同样的两个选择:维护一个复刻,或者取代它。维护复刻并不理想,原因与维护 React 复刻不理想一样,但有一点不同:lit-html 的范围要小得多,而且一般来说它是一个小得多的库。这将减少我们的工作量,使我们能够在需要时进行修复或改进。
替换 lit-html 将是一项工作,但比替换 React 要少得多:它在我们的代码库中纯粹是用来让我们的组件(重新)渲染 HTML。替换 lit-html 仍然意味着我们可以保留我们的业务逻辑,最终保持它们为最终用户提供的价值。Lit-Html 是我们系统中的一块小的“乐高砖”,React(或 Angular,或类似)是整个“盒子”。
第三方依赖项的成本
第三方依赖,无论大小,都会有一系列的成本,你的用户和/或开发人员将为此支付。每个人对这一成本是否值得的看法会有所不同,这取决于你的应用程序和技术栈,但当我考虑添加新的依赖项时,会出现以下一组成本:
包大小:这个依赖对我们必须在浏览器中交付和执行的最终 JavaScript 增加多少权重?对于这个依赖项所提供的东西来说,这个包的大小是否合适和值得?
突破性变化和升级:如果软件包有一个大的改动,需要工作来升级到最新版本,会发生什么?我们是继续使用旧版本(如果它没有得到更新或安全修复,那就不太理想),还是投入工作来升级?升级的工作是否可以很快得到优先考虑,或者是我们可能永远无法完成的工作类型?
未维护的代码或问题的风险:谁能说第三方的依赖项可能有特定的漏洞或问题而导致问题?(这并不是批评所有那些孜孜不倦地维护开源软件的人——但这些事情常会发生)。Jeremy Keith 在他最近关于信任的文章中指出:
你添加到项目中的每一个依赖项都是多一个潜在的单点故障。你自己的代码也是如此(把“依赖项”换成“文件”),但关键是你有完全的控制,你可能更熟悉它的工作原理,因为它是在内部编写的,而且你不需要对其他人负责来修复上游的问题。这并不是说你应该在每个项目上都重新创建一个世界;在自己构建和添加依赖项之间总是有一个很好的平衡,而且没有任何规则可以决定每次的正确结果。
总结
这篇文章并不是说你不应该接触依赖项。在回应 Jeremey Keith 关于信任和第三方依赖项的帖子时,Charles Harries 提出,跨浏览器兼容性在历史上是依赖项的驱动力。
浏览器兼容性是库(尤其是 Jeremy 提到的大库),如 React 和 Bootstrap--向开发者作出的基本承诺之一。我没有足够的时间在 caniuse.com 上浏览 Array.prototype.includes 或 MutationObserver 的页面。Lodash 在其主页的底部就承诺了跨平台兼容性。我完全同意 Charles 的观点,这也是为一个浏览器开发工具的工作具有优势的一个领域,因为我们了解用户对浏览器的偏好。
我希望,随着浏览器所支持的基线特性集越来越统一,尤其是 IE 浏览器的消亡,我们的行业可以随着时间的推移,在缺省的状态下,能够实现浏览器的广泛内置特性,在绝对必要的情况下进行多重填充,并把框架作为默认的起点。
作者介绍:
Jack Franklin,谷歌工程师,负责构建 Chrome DevTools。
原文链接:
https://www.jackfranklin.co.uk/blog/working-with-react-and-the-web-platform/
评论