为什么我不使用 Web Components

2025-05-26

为什么我不使用 Web Components

对于我在 dev.to 上的第一篇文章,我想写一个好的、安全的、没有争议的话题:Web 组件。

我写这篇文章主要是为了未来的自己,这样下次有人问我为什么对 Web 组件持怀疑态度,以及为什么Svelte默认不编译为自定义元素时,我就可以拿出点参考。(它可以编译为自定义元素,而且它可以使用自定义元素,这一点从它在“自定义元素无处不在”测试中取得的满分可见一斑。)

以上这些并非是对 Web Components 所付出辛勤努力的批评。我可能在这篇文章中犯了一些错误,如果可以,欢迎指正。

我也不是说你不应该使用 Web Components。它们确实有有效的用例。我只是解释了为什么不使用它们

1. 渐进式增强

这种观点可能越来越过时了,但我认为网站应该尽可能不使用 JavaScript。而 Web 组件则不然。

这对于本质上具有交互性的内容(例如自定义表单元素 ( <cool-datepicker>))来说没问题,但对于导航栏来说就不行了。或者考虑一个简单的元素,它封装了构建Twitter Web Intent<twitter-share> URL的所有逻辑。我可以用 Svelte 构建它,它会生成如下服务器渲染的 HTML:



<a target="_blank" noreferrer href="..." class="svelte-1jnfxx">
  Tweet this
</a>


Enter fullscreen mode Exit fullscreen mode

换句话说,这是一个普通的<a>元素,但却具有其可访问的荣耀。

启用 JavaScript 后,它会逐步增强——不再打开新标签页,而是打开一个小的弹出窗口。即使没有 JavaScript,它仍然可以正常工作。

相比之下,Web 组件 HTML 看起来像这样……



<twitter-share text="..." url="..." via="..."/>


Enter fullscreen mode Exit fullscreen mode

...如果 JS 被禁用或以某种方式损坏,或者用户使用的是旧版浏览器,那么它将毫无用处且无法访问。

正是这class="svelte-1jnfxx"一点使得无需 Shadow DOM 即可实现封装样式。这也引出了我的下一个观点:

2. CSS,呃……JS

如果要使用 Shadow DOM 进行样式封装,则必须将 CSS 包含在<style>元素中。唯一可行的方法(至少在避免 FOUC 的情况下)是将 CSS 包含在定义自定义元素的 JavaScript 模块中的字符串中。

这与我们一直以来得到的性能建议相悖,这些建议可以概括为“请少用 JavaScript”。CSS-in-JS 社区尤其因不将 CSS 放入.css文件中而受到批评,但我们却依然如此。

将来,我们或许可以使用CSS 模块可构造样式表来解决这个问题。我们或许还能使用Shadow DOM::theme::part设置其样式。但这些方法也并非没有问题。

3. 平台疲劳

在撰写本文时, Chromium 错误跟踪器https://crbug.com上有 61,000 个未解决的问题,这反映了构建现代 Web 浏览器的巨大复杂性。

每次我们向平台添加新功能时,都会增加复杂性——为错误创造新的表面区域,并且使得 Chromium 出现新竞争对手的可能性越来越小。

这也给开发人员带来了复杂性,他们被鼓励学习这些新功能(其中一些功能,如 HTML 导入或原始自定义元素规范,从未在 Google 之外流行起来并最终再次被删除。)

4. Polyfill

如果要支持所有浏览器,就必须使用 polyfill,这没什么用。一位 Google 员工(你好,Jason!)撰写的关于可构造样式表的文献并没有提到这是 Chrome 独有的功能(编辑:这个问题在我发起拉取请求后已经修复),这实在没什么用。三位规范编辑都是 Google 员工。Webkit似乎对设计的某些方面有所怀疑。

5. 构图

对于组件来说,能够控制其 slot 内容何时(或是否)渲染是很有用的。假设我们想使用该<html-include>元素在可见时显示来自网络的一些文档:



<p>Toggle the section for more info:</p>
<toggled-section>
  <html-include src="./more-info.html"/>
</toggled-section>


Enter fullscreen mode Exit fullscreen mode

惊喜!即使你还没有打开该部分,浏览器也已经请求了more-info.html,以及它链接到的所有图片和其他资源。

这是因为自定义元素中的 slotted 内容会立即渲染。而实际上,大多数情况下,我们更希望 slotted 内容能够延迟渲染。为了符合 Web 标准,Svelte v2 采用了这种立即渲染模型,但事实证明,这反而带来了很大的困扰——例如,我们无法创建与 React Router 等效的渲染方案。在 Svelte v3 中,我们放弃了自定义元素组合模型,并且从未改变过这种做法。

不幸的是,这只是 DOM 的一个基本特性。这引出了……

6. props 和 attribute 之间的混淆

道具和属性基本上是同一件事,对吗?



const button = document.createElement('button');

button.hasAttribute('disabled'); // false
button.disabled = true;
button.hasAttribute('disabled'); // true

button.removeAttribute('disabled');
button.disabled; // false


Enter fullscreen mode Exit fullscreen mode

我的意思是,几乎:



typeof button.disabled; // 'boolean'
typeof button.getAttribute('disabled'); // 'object'

button.disabled = true;
typeof button.getAttribute('disabled'); // 'string'


Enter fullscreen mode Exit fullscreen mode

然后还有一些名字不匹配......



div = document.createElement('div');

div.setAttribute('class', 'one');
div.className; // 'one'

div.className = 'two';
div.getAttribute('class'); // 'two'


Enter fullscreen mode Exit fullscreen mode

...还有一些看起来根本不对应的:



input = document.createElement('input');

input.getAttribute('value'); // null
input.value = 'one';
input.getAttribute('value'); // null

input.setAttribute('value', 'two');
input.value; // 'one'


Enter fullscreen mode Exit fullscreen mode

但我们可以忍受这些怪癖,因为在字符串格式(HTML)和 DOM 之间的转换中,一些东西肯定会丢失。这类怪癖的数量有限,而且都有文档记录,所以至少只要你有足够的时间和耐心,就能了解它们。

Web 组件改变了这一点。它不仅不再保证属性 (Attribute) 和 props 之间的关系,而且作为 Web 组件开发者,你(大概?)应该同时支持这两种属性。这意味着你会看到这样的情况:



class MyThing extends HTMLElement {
  static get observedAttributes() {
    return ['foo', 'bar', 'baz'];
  }

  get foo() {
    return this.getAttribute('foo');
  }

  set foo(value) {
    this.setAttribute('foo', value);
  }

  get bar() {
    return this.getAttribute('bar');
  }

  set bar(value) {
    this.setAttribute('bar', value);
  }

  get baz() {
    return this.hasAttribute('baz');
  }

  set baz(value) {
    if (value) {
      this.setAttribute('baz', '');
    } else {
      this.removeAttribute('baz');
    }
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'foo') {
      // ...
    }

    if (name === 'bar') {
      // ...
    }

    if (name === 'baz') {
      // ...
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

有时你会看到事情反过来——attributeChangedCallback调用属性访问器。无论哪种情况,其人机工程学都是灾难性的。

相比之下,框架有一种简单且明确的方式将数据传递到组件。

7. 漏水设计

这一点有点模糊,但让我感到奇怪的是,它attributeChangedCallback只是元素实例上的一个方法。你可以这样做:



const element = document.querySelector('my-thing');
element.attributeChangedCallback('w', 't', 'f');


Enter fullscreen mode Exit fullscreen mode

虽然属性没有改变,但行为却和改变了一样。当然,JavaScript 一直以来都提供了很多恶作剧的机会,但当我看到实现细节像这样被戳穿时,我总觉得它们好像在试图告诉我们设计不太对劲。

8. DOM 很糟糕

好吧,我们已经确定 DOM 很糟糕。但对于构建交互式应用程序来说,它是多么尴尬的接口,怎么说都不为过。

几个月前,我写了一篇名为《写更少的代码》的文章,旨在说明 Svelte 如何让你比 React 和 Vue 等框架更高效地构建组件。但我没有将它与 DOM 进行比较。我应该这样做。

回顾一下,这是一个简单的<Adder a={1} b={2}/>组件:



<script>
  export let a;
  export let b;
</script>

<input type="number" bind:value={a}>
<input type="number" bind:value={b}>

<p>{a} + {b} = {a + b}</p>


Enter fullscreen mode Exit fullscreen mode

这就是全部内容。现在,让我们构建一个同样的东西作为 Web 组件:



class Adder extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: 'open' });

    this.shadowRoot.innerHTML = `
      <input type="number">
      <input type="number">
      <p></p>
    `;

    this.inputs = this.shadowRoot.querySelectorAll('input');
    this.p = this.shadowRoot.querySelector('p');

    this.update();

    this.inputs[0].addEventListener('input', e => {
      this.a = +e.target.value;
    });

    this.inputs[1].addEventListener('input', e => {
      this.b = +e.target.value;
    });
  }

  static get observedAttributes() {
    return ['a', 'b'];
  }

  get a() {
    return +this.getAttribute('a');
  }

  set a(value) {
    this.setAttribute('a', value);
  }

  get b() {
    return +this.getAttribute('b');
  }

  set b(value) {
    this.setAttribute('b', value);
  }

  attributeChangedCallback() {
    this.update();
  }

  update() {
    this.inputs[0].value = this.a;
    this.inputs[1].value = this.b;

    this.p.textContent = `${this.a} + ${this.b} = ${this.a + this.b}`;
  }
}

customElements.define('my-adder', Adder);


Enter fullscreen mode Exit fullscreen mode

是的。

另请注意,如果您同时更改a和,将导致两次单独的更新。框架通常不会遇到此问题。b

9. 全局命名空间

我们不需要过多地思考这个问题;可以说,人们已经充分认识到单一共享命名空间的危险。

10. 这些都是已解决的问题

最令人沮丧的是,我们已经有了非常好的组件模型。我们仍在学习,但基本问题——通过面向组件的方式操作 DOM 来保持视图与某些状态同步——多年来已经得到了解决。

然而,我们正在向平台添加新功能,只是为了让 Web 组件与我们在用户空间中已经可以做的事情保持一致

由于资源有限,花在一项任务上的时间就意味着没有时间花在另一项任务上。尽管开发人员群体普遍对 Web 组件漠不关心,但 Web 组件却耗费了相当多的精力。如果这些精力用在其他地方,Web 会取得怎样的成就?

文章来源:https://dev.to/richharris/why-i-don-t-use-web-components-2cia
PREV
如何获取住宅代理
NEXT
保持警惕