为什么我不使用 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>
换句话说,这是一个普通的<a>
元素,但却具有其可访问的荣耀。
启用 JavaScript 后,它会逐步增强——不再打开新标签页,而是打开一个小的弹出窗口。即使没有 JavaScript,它仍然可以正常工作。
相比之下,Web 组件 HTML 看起来像这样……
<twitter-share text="..." url="..." via="..."/>
...如果 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>
惊喜!即使你还没有打开该部分,浏览器也已经请求了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
我的意思是,几乎:
typeof button.disabled; // 'boolean'
typeof button.getAttribute('disabled'); // 'object'
button.disabled = true;
typeof button.getAttribute('disabled'); // 'string'
然后还有一些名字不匹配......
div = document.createElement('div');
div.setAttribute('class', 'one');
div.className; // 'one'
div.className = 'two';
div.getAttribute('class'); // 'two'
...还有一些看起来根本不对应的:
input = document.createElement('input');
input.getAttribute('value'); // null
input.value = 'one';
input.getAttribute('value'); // null
input.setAttribute('value', 'two');
input.value; // 'one'
但我们可以忍受这些怪癖,因为在字符串格式(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') {
// ...
}
}
}
有时你会看到事情反过来——attributeChangedCallback
调用属性访问器。无论哪种情况,其人机工程学都是灾难性的。
相比之下,框架有一种简单且明确的方式将数据传递到组件。
7. 漏水设计
这一点有点模糊,但让我感到奇怪的是,它attributeChangedCallback
只是元素实例上的一个方法。你可以这样做:
const element = document.querySelector('my-thing');
element.attributeChangedCallback('w', 't', 'f');
虽然属性没有改变,但行为却和改变了一样。当然,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>
这就是全部内容。现在,让我们构建一个同样的东西作为 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);
是的。
另请注意,如果您同时更改a
和,将导致两次单独的更新。框架通常不会遇到此问题。b
9. 全局命名空间
我们不需要过多地思考这个问题;可以说,人们已经充分认识到单一共享命名空间的危险。
10. 这些都是已解决的问题
最令人沮丧的是,我们已经有了非常好的组件模型。我们仍在学习,但基本问题——通过面向组件的方式操作 DOM 来保持视图与某些状态同步——多年来已经得到了解决。
然而,我们正在向平台添加新功能,只是为了让 Web 组件与我们在用户空间中已经可以做的事情保持一致。
由于资源有限,花在一项任务上的时间就意味着没有时间花在另一项任务上。尽管开发人员群体普遍对 Web 组件漠不关心,但 Web 组件却耗费了相当多的精力。如果这些精力用在其他地方,Web 会取得怎样的成就?
文章来源:https://dev.to/richharris/why-i-don-t-use-web-components-2cia