似乎 Web Components总是差一点就要流行起来了。它们就像前端极客们的桌面 Linux 时代。我不断看到社交媒体上冒出的关于 Web Components的最新文章,总是希望能有我遗漏的重要内容,让它们变得更有实质性,但我每次都感到失望。我在 2020 年写下了我对 Web 组件的看法,而直到现在,关于它的讨论似乎没有任何进展。这就像是永恒的“九月”(Eternal September),人们一直回到 Web 组件最初的承诺上,尽管现实早已证明它远未达到预期。
哪里出了问题
为了概括我之前的文章:
Web Components的宣传点是“从整个网络获取语义元素!”,但这是错误的解决问题的方式。
- 自定义元素并不“语义化”,因为搜索引擎不知道它们的含义。
- “从整个网络”获取元素对于用户性能来说,永远不如每个站点使用一个统一的、支持渐进增强的 JavaScript 框架来得好。
-
customElements.define
是一个非常笨拙的 API。 -
<template>
和<slot>
不错,但它们无法替代真正的模板语言。 - Shadow DOM 也很笨拙,只解决了一个非常小众的问题。
好了,这就是基本情况,我们能否达成一致?
是的,在某些情况下,使用 customElement
API 是为元素命名空间并确保懒加载元素调用其构造函数的便捷方式,但它们主要在像在富文本内容中渲染第三方嵌入时才有用。这并不足以让“Web 组件”这个名字有实质性的含义。
如果你将“Web Components”这个名字放在一个由轻量框架驱动的 JavaScript 小部件集合上,比起用别的名字,会吸引更多的人去查看它们。但归根结底,这只是几个笨拙的 DOM API 的营销名称,以及我们都希望真正存在的一个梦想。这个梦想是,你可以随意混合使用小部件,无论每个小部件是用什么框架编写的,但现实是,唯一的办法是为页面上包含的每个 Web 组件付出引入一个框架的代价。这永远不会成为用户性能至关重要的网站的实际选择。
浏览器厂商的救赎
好消息是,尽管“Web Components”并不是真正的一个“事物”,浏览器厂商最终会用正确的方式解决真正的问题,尽管有时会需要一段时间。
就像我当时写 Shadow DOM 时所说:
Shadow DOM 的核心作用是允许页面的某个元素拥有自己的 CSS 重置。我们完全可以将其作为 CSS 本身的一部分(或许通过改变 @import 的规则,允许其嵌套使用,而不仅限于顶层)。
事实证明,这正是发生的事情。因此,一旦浏览器对 CSS 的“甜甜圈作用域”(donut scoping)支持足够广泛后,几乎没有理由再使用 Shadow DOM。使用 CSS 作用域选择器和导入层来为组件创建一个作用域内的 CSS 重置,你将获得 Shadow DOM 的所有好处,而无需担心“light DOM 样式穿透 shadow root”等晦涩的术语。
那么,浏览器厂商应该做什么,而不是投入更多精力去改进 Web 组件?
我认为大家都同意,DOM 进化的最佳案例是 jQuery 与 querySelectorAll 之间的关系。2003 年,Simon Willison 构建了一个 document.getElementsBySelector
的演示版本。jQuery 采用了这个想法,发展出曾经无处不在的 $("...")
API。随后,浏览器厂商将其转化为 document.querySelectorAll
,使其变得更快,并且无需依赖 jQuery 即可普遍使用。
这是 web 进化的理想情景:开发者通过多年的集体努力,找到了解决重复问题的办法,浏览器厂商将其原生化,使其更加高效、普遍可用。相比之下,customElement
和 Shadow DOM API 起源于大约十年前,在许多现代 JavaScript 框架技术出现之前。因此,它们由于当时我们整个行业在构建大型 JavaScript 应用框架方面的经验不足,最终解决了错误的问题。
与其继续改进现有的 Web 组件,浏览器厂商应该关注的是找到新的领域,在这些领域中我们可以标准化开发者已经在做的事情。我有三个建议供他们参考。
响应式
最近,Nolan Lawson 写了一篇文章《通过构建一个框架来学习现代 JavaScript 框架的工作原理》。在文章中,Lawson 将“现代”框架定义为具有以下三大组件:
- 使用响应式(例如信号)来更新 DOM。
- 使用克隆的模板来渲染 DOM。
- 使用现代 Web API,如
<template>
和Proxy
,使上述过程更加轻松。
老实说,我在 Lawson 的文章发表之前就开始起草这篇博文了,所以很高兴看到他突出了我也计划在此讨论的现代框架的一些相同方面。<template>
和 Proxy
API 很好,浏览器厂商不需要特别改进它们。但在改善响应式/信号和 DOM 渲染方面,仍有很大的空间。让我们先谈谈响应式。
响应式的基本思想是,如果你有一个简单的组件,比如待办事项,你想知道 todo.done
从 false
变为 true
时,以便触发其他数据的更改,比如 todos.count
从 N 变为 N+1。在我 2020 年的文章中,我称之为“数据生命周期”。
Lawson 的文章中,他使用大约 50 行 JavaScript 创建了一个快速简陋的响应式系统,但更现实的响应式系统,比如 observable-membrane
、@vue/reactivity
和 @preact/signals
要复杂得多。
将这些系统的核心从 JavaScript 库中移到浏览器中,可以显著优化响应式数据元素之间的依赖树。要为标记节点为脏数据而编写一个性能优化的算法需要大量代码。对于一个只有少数响应式元素的简单待办应用,响应式引擎的复杂性很容易压过应用本身的复杂性。但如果由浏览器直接解决这个问题,它可以用 C++ 或 Rust 编写这段代码,使其达到最高效率,因为不需要每次页面加载时都重新打包、发送给客户端并解释。由浏览器团队支付一次成本,全球的 Web 开发者和最终用户都能享受其带来的好处。
显然,这是一个已经被多个框架独立解决的问题。现在该轮到浏览器来解决它了。
更新:Dave Rupert 在 Mastodon 上指出,W3C 已经有一个提案,计划将响应式信号加入 JavaScript,并正提交至 TC39。我祝他们在提案过程中好运。
合成 DOM / 虚拟 DOM
现代框架的另一个亟需优化的方面是 DOM 渲染。在 Lawson 的文章中,他花了大量时间解释如何将代理调用转换为高效的 DOM 更新。就我而言,我不认为让浏览器厂商介入模板大战是明智的。JavaScript 中现有的模板字面量语法已经足够让开发者创建适合他们的模板系统,或者他们可以继续使用 JSX 之类的解决方案,将模板作为框架的编译时功能。真正需要的是一种将模板系统生成的 HTML 结果与页面现有浏览器 DOM 高效合并的方法。如果只是替换一些简单的 HTML,编写 node.innerHTML = newHTML;
的性能是相当不错的。问题在于,如果节点有事件处理程序(可以通过委派给 DOM 中更高层的处理程序部分缓解),或者有需要在多次渲染中保留的状态,比如输入元素,如果使用 node.innerHTML
替换其父元素之一的内容,会导致输入状态丢失,已输入的文本消失。所有用于模板化交互页面的系统都需要一种方法来防止在每次渲染时出现这些问题,确保仅覆盖需要被覆盖的状态。
React 通过使用虚拟 DOM 与浏览器 DOM 进行协调,著名地解决了这个问题,但即便是像 HTMX 这样侧重于服务器的框架,也需要一种在不破坏当前元素状态的情况下替换 DOM 节点的解决方案。类似的另一种解决方案是已有八年历史的 MorphDOM。最近,Caleb Porzio 正在开发 @alpinejs/morph 作为此问题的解决方案,他偶尔会在播客中讨论他遇到的复杂边缘情况。Svelte 通过将尽可能多的协调工作推向其编译器来绕过虚拟 DOM 的问题,但即使是 Svelte,也有些工作必须在客户端上完成。不管怎样,编译器端的解决方案固然不错,但没有构建步骤就能工作的解决方案也很重要。
无论你如何应对它,创建两个 DOM 树之间的差异可能是一个非常复杂的过程,因为确定一个节点是完全替换了还是仅仅移动到树中的另一个位置是相当困难的。这涉及到大量的边缘情况和权衡考虑。这是另一个浏览器厂商可以整合的领域。如果浏览器有一个能够协调两个 DOM 树的 API,它会更快更高效。拥有这样的浏览器 API 将使框架不再在渲染性能上相互竞争,而是专注于其 API 的便利性和生态系统的深度。这是另一个已经被广泛探索的领域,准备被铺平。
自适应大小的 iframe
让我们谈谈另一个 Lawson 文章中未提到的常见开发者需求,以及一个我认为很多开发者忽视了的技术,因为它被认为是遗留技术。
谦逊的 iframe 可能可以追溯到 Internet Explorer 4,但它实际上比 Shadow DOM 更强大,因为它们具有更强的样式隔离和一些真正的安全保证。因此,尽管 iframe 在现代 JavaScript 框架中几乎没有角色,但它们在我所在的在线新闻出版领域仍然非常常见。如果你有一个内容槽,并希望能够在其中间放置某种小部件,并让它在 CMS 和未来的重设计中工作,你会使用 iframe。示例包括 DataWrapper 和 Flourish 的图表,YouTube 和 Vimeo 的视频等。
iframe 的大问题是 CSS 中没有办法告诉它根据其内容自动调整高度。这样设计有合理的原因。例如,如果主机页面能通过添加 iframe 并检查它的高度来确定你是否登录了银行网站,那将非常糟糕。因此,制作自适应大小的 iframe 最好的方法是使用 JavaScript 将消息从包含页面传递给主机页面,告诉主机页面包含页面的大小,以便主机可以相应地调整 iframe 的大小。有各种库可以用来完成这项任务,比如 NPR 的 Pym.js 和 SideChain 或 @newswire/frames。
尽管有安全问题,我认为通过原生解决方案可以绕过这些问题,在这种解决方案中,框架和被框架的页面使用 X-Frame-Options
头或 CSP 来告诉浏览器哪些站点想要合作允许动态调整 iframe 的高度。将这项功能从 JavaScript 中移到浏览器中,将修复许多因轮询窗口大小变化或包含带有 vh
CSS 样式的页面而产生的小问题,并简化任何需要创建或使用嵌入的人的工作。这也将使在 JavaScript 不被允许或需要被严格限制的情况下,能够使用自适应 iframe 成为可能。
总结
希望大家明白我在主张什么。Web Components的梦想是美好的,但我们需要新的解决方案来应对现代 Web 开发者遇到的问题,而不仅仅是不断重走十年前的老路。
我让 Claude 为这篇博文写一个令人惊叹的结尾,这是它想出来的:
“‘Web 组件’的承诺未能实现它在整个网络中激发的想象力。与其尝试将新 API 套用到旧问题上,我们应该展望未来——跟随开发者已经铺设的需求路径。
反应性、虚拟 DOM 协调和自适应大小的 iframe 都是开发者多年来努力寻找解决方案的领域。现在是时候让浏览器厂商铺平这些道路了——将开发者创造的最佳成果变得更快、更高效,并普遍可用。
这就是 Web 真正进步的方式——不是从高高在上的自上而下的想法开始,而是从现实的解决方案有机地满足实际需求。如果浏览器厂商能够转向支持开发者已经在做的事情,我们将会拥有更好的工具。解决方案可能并不总是那么性感,但它们将植根于实际应用。
所以,让我们从过去中学习,面对现实,并构建我们所需要的网络——而不是我们想象中的那个。只要我们有远见,拼图的各个部分已经在慢慢组合。”
最后,让我们继续为 Web 的未来开辟新道路,在即将到来的 AI 编程机器人让我们失业之前。