Web 组件并非未来

2025-05-27

Web 组件并非未来

几年前,我写了一篇文章,指出 Web Components 可能不是 Web 开发最有利的方向。

这篇文章以一种温和的方式审视了他们的观点,指出了哪些地方合理,哪些地方不合理。它并非旨在挑起“我们对抗他们”的争论,我希望人们能够得出合理的结论。

但在过去几年里,我看到的情况却越来越糟。有些人可能从未认真对待过 Web 组件,但我一直都认真对待。我在生产环境中使用了它们长达 7 年。早期,我为 Shadow DOM 之类的功能编写了几个 polyfill,以便在它们达到黄金时代之前使用它们。

但我当时以及之后的经历只能让我得出一个结论:Web Components 可能是我所能预见的对 Web 未来的最大威胁。


乌托邦的愿景

图片描述

我承认这种说法听起来有点牵强。但我之所以相信这一点,背后有很多原因。首先要理解 Web 组件的预期优势。

Web 组件的愿景是,有一天,无论你使用什么工具编写组件,你都可以拥有像 DOM 元素一样原生的组件,并且可以添加到任何网站,而无需考虑网站的编写方式。无需再考虑具体的构建工具、具体的运行时机制,也无需再考虑与这些组件的交互方式。

从某种意义上说,这是一个可移植、可互操作的网络。它可以减轻未来的迁移需求。它可以让你为任何可能的未来做好准备。这是让您的网站和应用程序面向未来的理想方式。

这前景多么诱人,不是吗?这正是 Web 组件如此诱人却又如此危险的原因。


竞争标准

图片描述

我不需要翻看老掉牙的 xkcd 漫画,你就能明白标准的挑战所在。标准越雄心勃勃,就越有可能遭遇反对或其他实现方式。标准化之后,这种挑战并不会消失。它只是表明,存在一种“幸福之路”。你可以选择接受,也可以选择放弃。

如果单从 JavaScript 框架的数量就能看出,我们距离就 Web 组件的编写方式达成共识还很遥远。即使我们今天已经接近了,但十年前也远未达到这个程度。

引入更高级的原语可以产生积极的影响。突然间,一些原本困难的事情变得更容易了。这最初会引发更多的探索。Web Components 在 2010 年代中期推动了 JavaScript 框架数量的增长。这对我来说是创建 SolidJS 的重要灵感来源。类似的例子还有由于 Vite 而构建的 Metaframeworks 数量的增加。

但它也可能产生负面影响。如果假设太多,探索其他领域就会变得更加困难,因为一切都围绕着既定的模式。还有什么比一个永远不变的网络标准更稳固呢?

除了 Web 标准之外,我也为此苦苦挣扎过很多次。根据规范,JSX 没有既定的语义,但我试图说服市面上各种各样的工具,他们假设得太多。我只能想象,如果 JSX 在浏览器中标准化,那将是一场噩梦。别忘了像 Inferno、Solid 和 Million 这样的框架,它们的 JSX 转换做得比这更好,就连 React 也一直在不断改变它们的转换方式。

这只是众多例子中的一个。对我们有帮助的东西,实际上也可能束缚我们的手脚。在标准化任何更高级别的机制时,我们必须谨慎行事,因为它假设了很多。当它的存在影响到我们对整个平台的看法时,仅仅说不是每个人都必须使用它,是不够的。


机会成本是真实存在的

作为一名框架作者,我深谙此道。我常说,在这个领域,事物的发现和发明一样多。我的意思是,设计决策存在着某种真理/物理规律,遵循这些规律,我们最终会走向相似的境界。这些工具并非明目张胆地互相抄袭,而是各自遵循着各自的规律。

出于同样的原因,一旦发现改变了我们的观点,损害在我们编写任何一行代码之前就已经造成。如果你的工作是设计一个系统,你肯定不希望有多余的部分。你想要清晰的目标界限。与其对同一事物进行上百万次的修改,不如尝试重复使用一个事物。更重要的是,要认识到,要完成常见的任务,你需要多个部分,这些部分相互交织。

例如,React 开发者们肯定能感受到从 2017 年 Suspense 发布到 2022 年 RSC 数据获取方案之间间隔了多久。为什么这花了 5 年时间?因为这不是一条直线。需要一段时间才能理解所有部分是如何衔接的。这本身是合理的。但更重要的是,React 不想分段发布,直到他们确定了整个方案的答案。随着他们研究的深入,发现所有这些部分都是相互关联的,虽然它们可以被拆分,但它们需要彼此配合才能构成一幅完整的图景。

RSC 并不符合每个人对在 React 中使用 Suspense 进行数据获取的理解。或许,人们可以从客户端数据获取原语中受益。React 在这方面雄心勃勃,这是他们作为工具的权利,并且决定了最佳方案,但这也可能带来多种结果。

作为一名开发者,我始终可以选择不使用 React。虽然我可以选择不使用某些 React 功能,但显然 React 中的所有内容都已迁移到其当前的思维模型。我甚至希望能够轻松地从 React 迁移过来。

但这里有一个很大的区别。React 是一个库,而不是一个标准。当涉及到标准时,这些选项是不一样的。如果我只想要作用域样式,而现在我必须处理 Shadow DOM,因为那是最适合 Web Components 实现单一处理方式的抽象,那么我就只能这么做了。

当原语超出其预期用途,当它们过度抽象时,你就无法挽回了。你已经付出了代价。任何做过项目重大架构重构的人都可以证明,最困难的部分是调整边界。如果事物属于同一逻辑分组,它们就更容易更新。当你需要分解事物时,事情才真正变得具有挑战性。你总是可以添加另一层抽象来解决问题,但移除一层抽象却可能很困难。


抽象的代价

图片描述

所以,Web 组件的根本问题在于它们建立在自定义元素之上。元素 !== 组件。更具体地说,元素是组件的子集。有人可能会说,每个元素都可以是组件,但并非所有组件都是元素。

那又怎样?这意味着每个界面都需要通过 DOM。有些界面采用定义明确但并非完美适配的方式,而有些界面则采用新定义的方式,这些方式会增强或改变元素的处理方式,以适应扩展的功能。

首先,DOM 元素具有属性 (attribute) 和特性 (property)。这样它们才能用 HTML 表示。属性 (attribute) 仅接受字符串,而特性 (property) 作为 JS 接口可以处理任意值。原生 DOM 元素针对特定属性 (attribute)/特性 (property) 制定了许多规则,例如,有些属性 (attribute) 是布尔值(存在即适用),而有些则是伪布尔值(需要显式设置“true”/“false”)。有些特性 (property) 会反映到属性 (attribute) 中,而有些则不会。

模板语言的目标之一是以统一的方式解决这个问题。我们可以围绕已知的元素和属性制定特殊规则。但对于自定义元素,我们一无所知。这就是为什么一些模板库会使用一些有趣的前缀来指示应该如何设置。即使是 Solid 的 JSX,我们也有attr:prop:bool:前缀,原因就在于此。现在,每个运行时位置和编译器钩子都需要意识到这一点。

您可能认为我们需要将更好的模板作为标准。但就像上面的 JSX 一样,您需要考虑这一决定的影响。几年前,大多数人可能都会认为像 LitHTML 这样的模板渲染方式很好。其他解决方案也可以输出它。然而,在此期间,我们意识到像 Solid 这样的响应式渲染性能更胜一筹。它通过改变模板的语义来实现这一点。如果我们当时更进一步,那么我们制定的标准就不再是实现模板的最佳方式了。

事情远不止于此。DOM 元素可以被克隆。但自定义元素的行为有所不同,这意味着它们应该被导入。它们拥有基于 DOM 的生命周期,可以根据升级时间同步或异步触发。这会对诸如响应性跟踪和上下文 API 之类的功能造成严重破坏。然而,这些细节对于与原生 DOM 行为和事件的交互至关重要。而 JavaScript 组件则无需担心这些。

还有一些其他特性,例如 Shadow DOM 中事件定位的工作方式。有些事件不会“组合”,即不会超出 Shadow DOM 边界冒泡。有些事件不会持续冒泡,因为它们无法识别不同的目标,例如“focusin”,因为无论哪个子元素获得焦点,Shadow Host 始终会被设为目标。我们可以花几天时间讨论这些问题,但我不想在这里转移太多注意力。有些是今天的缺陷,有些是设计使然。但它们的共同点是,都需要进行一些特殊的考虑,否则这些考虑是不必要的。

当然这会产生性能开销:

但即使你认为这种性能成本微乎其微,当我们在服务器端进行诸如 SSR 之类的操作时,它又会给我们带来什么呢?是的,你完全可以使用 Web Components 来实现 SSR。Hydration 也完全可以实现。但它们就像一个在没有 DOM 的地方的 DOM 接口。你可以创建一个最小的包装器来处理大多数事情,但这都是额外的开销。这一切都是因为我们试图让组件成为它们本来就不是的东西。

在服务器上,没有这样的标准。我们又回到了有具体解决方案的时代。它只是另一种框架,并不比我为下一个项目选择 Vue 或 React 更有保障。这本身并没有错,但我们需要认识到这个事实。


Web 组件的实际成本

总体而言,处理原生元素的复杂性随着 Web 组件新发现的灵活性而增加。随着工具对原生元素的支持越来越好,付出代价的不仅是工具本身,还有所有使用该工具的用户。需要编写更多代码,执行更多代码来检查这些边缘情况。这就像一笔影响每个人的隐形税。

我之前讨论过早期标准化会带来哪些灾难。但它也有可能扼杀未来某些方面的创新,因为它假设太多。数据融合的改进,例如可恢复性、部分数据融合或选择性数据融合,都依赖于事件委托才能工作。但如果 Shadow DOM 干扰了这一点,那么 Web 组件又如何适应这种模型呢?有人可能会说,SSR 是一个疏忽,因为我们在 2013 年并没有考虑太多,但随着时间的推移,这种差距只会越来越大。

如果说编译器和构建工具的进步有什么不同的话,那就是我们正在朝着不再仅仅关注开发者体验的方向发展。组件只是你在开发时拥有的东西,最终会在输出中消失。为了获得最佳的用户体验,我们会优化掉这些组件。

我并不是说没有人能找到一些有趣的解决方案来应对这些问题,但它们都意味着要承担由于错误抽象导致的基础错位而产生的隐性成本。这正是导致对话如此困难的原因。这不是你可以改进的东西。这只是很多事情上的错误。

现在我们可以说,不同的解决方案有不同的权衡。或许像 Web 组件这样的方案只有在标准机构的支持下才有可能成功,因为它需要普及性才能发挥作用。这是我们正在努力实现的理想,只是目前还未完全实现。

但它真的是理想的吗?


重访乌托邦

图片描述

在讨论微前端或微服务时,我们经常会遇到类似的争论。将项目与开发人员协调一致有利于组织目标的实现。这符合康威定律

Web 组件提供的隔离性或可移植性意味着页面上可以有来自不同来源的多个不同组件。现在,像其他组件一样,谨慎是必要的。就像不想用不同的语言编写所有微服务一样,您可能也不想在不同的框架中编写所有组件。

但前端是一个限制性更强的领域。每千字节的 JS 代码成本都相当高昂。你不应该混搭各种代码,不仅是因为维护成本,也是为了减少负载。而这正是问题开始出现的地方。

如果您的目标是面向未来,那么您需要做好准备,即使是同一个库,也要在同一页面上保留不同版本。Lit 和 React 的情况都一样。您可以选择永远不更新任何内容,但这在没有 Web Components 的情况下也是一种选择。这里没有额外的保证。为了面向未来,唯一的选择是冻结内容、维护多个版本并在页面上加载更多 JS。

实际上,你会同步更新你的库,任何库都是如此。在这种情况下,如果你的页面上只有一个库,Web Components 不会为你做任何事情,只会增加开销。这可能会妨碍库现在和将来提供的功能。你最好还是使用不带 Web Components 的库。

第二个考虑因素是粒度。如果你有一个微前端,那么它就是一个可替换的组件。如果将来你认为它不是处理问题的最佳方式,你可以将其替换掉。但是,一旦你在所有地方都采用了 Web 组件,你就需要处理每个接触点。Web 组件与微前端和微服务不同,因为它们可以跨部门使用。这对标准化很有帮助,但我从未见过使用 jQuery 的公司能够完全摆脱它。

Web 组件最引人注目的用途是作为一种微前端容器。在这种情况下,你无需支付扩展成本,外部通信量极小,并且易于切换。一次性场景。不过,在这些情况下,摩擦足够低,因此无需使用 Web 组件。出于人体工程学考虑,我会选择在页面上放置一个 Zendesk 小部件,但这种抽象是否值得付出这样的代价?


结论

这就是关键所在。我承认在某些情况下使用 Web 组件确实有人体工程学上的好处,但其带来的成本却非常高昂。虽然我不应该因为一项技术能够实现各种糟糕的模式就轻易否定它,但当它从来都不适合理想的场景时,我很难支持它。充其量,它只是一笔微不足道的开销。

Web 组件本身就是一种妥协。我们知道,有时我们需要妥协。但这没什么好兴奋的。有些妥协确实比其他的更好。

有人告诉我,10年后可能没人用Solid了,但Web Component仍然会存在,界面和现在一样。但我思考了一会儿,决定还是用Solid,因为就目前而言,它是最佳选择。10年后,即使我不得不使用10年前的Solid版本,它也比Web Component版本更好。10年不会抹杀这一点。希望10年后我能用上更好的东西。

这个决定完全是正交的。所以从某种意义上说,Web 组件本身并没有什么问题,因为它们只能是它们本来的样子。真正危险的是,它们承诺成为它们本来不是的样子。它们的存在扭曲了周围的一切,将整个网络置于危险之中。这是每个人都必须付出的代价。

文章来源:https://dev.to/ryansolid/web-components-are-not-the-future-48bh
PREV
TypeScript 实用类型:6 种最有用的
NEXT
您一直想要的正则表达式 (RegEx) 备忘单 感谢您的阅读