为什么我不使用 Web 组件?
Web 组件是指对 Web 的数据和方法进行封装实体,它的到来,方便了很多开发者对业务逻辑方面的处理,不过也有人认为,Web 组件并不适合自己。
作者 | Rich Harris
译者 | 宋蕾,责编 | 屠敏
出品 | CSDN(ID:CSDNnews)
本文我要写一个好一点,没有争议的安全话题:Web组件。
我写这个主要是为将来的自己,下次有人问我,我能说点什么,关于为什么对Web组件持怀疑态度,以及为什么Svelte默认不编译自定义元素(它可以编译CEs(自定义元素),且可以使用CEs作为它在自定义元素无处不在上良好成绩的证据)。
这些都不应该被视为对Web组件所做的辛苦工作的批评,我可能在这篇文章里犯了一些错误,如果有这种情况,欢迎大家更正。
我也不是说你不应该使用Web组件,它们确实有有效的使用案例,我只是解释为什么我不使用。
渐进增强
渐进增强可能是一个越来越过时的观点,但我认为网站应该尽可能在没有JavaScript的情况下能正常使用。但是Web组件不可以。
使用Web组件,对于本质上是交互式的元素来说是很好的,比如自定义表单元素(<cool-datepicker>),但是对于导航栏来说就不是很好。或者考虑一个简单的<twitter-share>元素,它封装了所有构建Twitter网站意向URL的逻辑,我可以在Svelte中构建它,且它将生成服务器端渲染HTML,如下所示:
<a target="_blank" noreferrer href="..." class="svelte-1jnfxx"> Tweet this </a>
换句话说,一个标准的<a>标签,就是放可访问的链接。
启用了JavaScript后,它会逐渐增强——不再是打开一个新的选项卡,而是打开一个小的弹窗。但是如果没有开启,它仍然可以正常使用。
相比之下,HTML的Web组件看起来像下面这样:
<twitter-share text="..." url="..." via="..."/>
如果JS被禁用或者出现一些问题,再或者用户使用较旧的浏览器,这些Web组件会变得不可用和不可访问。
class= "svelte-1jnfxx"是为了启用没有Shadow DOM(影子DOM)封装好的样式,这让我想到了下一个点:
CSS在JS中
如果你想使用Shadow DOM进行样式封装,你必须使用<style>标签包裹你的样式。这是唯一可行的办法,至少如果你想避免FOUS(浏览器样式闪烁或者无样式内容闪烁),必须把css放在定义自定义元素的javascript模块的字符串中。
这与我们给出的性能建议背道而驰,该性能建议可以总结为“请使用更少的javascript“。特别是CSS-in-JS社区因为没有将css放在.css文件中,而受到批评,但是呢,此一时彼一时。
将来,我们可能可以使用CSS 模块以及可结构化的样式表一起来解决这个问题,我们可能可以使用::theme和::part在Shadow DOM中来设置样式,但这些也不是没有问题的。
平台疲劳
在写这篇文章时,https://crbug.com上,上有61,000个未解决问题,Chromium bug跟踪器反映了编写现代web浏览器的极大复杂性。
每次我们向平台添加新特性时,都会增加其复杂性——为bug创建新区域,并使Chromimu的新竞争对手越来越不可能出现。
它还为开发工程师带来了复杂性,他们被鼓励学习这些新特性(其中一些功能,例如html导入或者原始自定义元素规范,这些永远不会在Google之外流行,且最终会再次被删除。)
垫片
如果你想支持所有的浏览器,使用polyfills(垫片)也没有作用。谷歌员工(hi Jason!)撰写的结构化样式表文献真的没有帮助,因为它没有提及仅限Chrome浏览器的特性(三位标准的制定者都是谷歌员工,Webkit似乎对设计的某些方面存在有些怀疑。)
组件构成
一个组件能够控制何时(或是否)其插槽内容被渲染是有必要的,假设我们想使用<html-include>标签来展示网络上的可见时显示的一些文档。
<p>Toggle the section for more info:</p> <toggled-section> <html-include src="./more-info.html"/> </toggled-section>
令人意想不到的是,即使你尚未切换打开该部分,浏览器已经请求了more-info.html文件,以及它链接到的其他所有图像和资源。
这是因为插槽内容在自定义标签中饥饿渲染,事实证明,大多数情况下,你希望插槽内容能懒惰渲染。为了和web标准保持一致,Svelte v2版本采用饥饿模式,但是事实证明,它成为一个主要的阻碍源——我们无法创建和React Router等效的功能,例如,在Svelte v3版本里面,我们放弃了自定义元素结构模型,且从不回头。
不幸的是,这仅仅只是DOM的一个基本特性,这让我们….
容易混淆的props和attributes
Props和attributes基本是同一个概念,对吧?
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组件改变了这一点,不仅不再保证attributes和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') { // ... } } }
相比之下,框架有一种简单而明确的方式将数据传递到组件中。
有时你会看到其他方式——attributesChangedCallback调用属性访问器,但是无论哪种方式,对提高工作效率简直是灾难性的。
设计漏洞
这一点有些模糊,但令我诧异的是,attributeChangedCallback仅仅是元素实例的一个方法,你可以按照下面这样做:
const element = document.querySelector('my-thing'); element.attributeChangedCallback('w', 't', 'f');
没有属性改变,但是它和有改变的行为是一样。当然,javascript总是提供大量机恶作剧机会,但是当我像这样查看实现细节时,我总是觉得好像他们在尝试告诉我们设计不太正确。
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,会导致两个独立的更新,框架通常不会受这个问题影响。
全局命名空间
我们不需要过多地讨论这一点;这足以说明使用单个共享命名空间的危害在一段时间已经被很好理解了。
已经解决的问题
最大的挫败感是我们已经有非常好的组件模型。我们仍然在学习,但是通过以面向对象的方式操作DOM来保持视图和一些状态同步的基本问题已经解决了多年。
我们正在向平台添加新特性,只是为了让web组件和我们在用户端已经做过的工作保持一致。
由于资源有限,花在一个任务上的时间意味着没有花在另一项任务上,考虑到开发工程师数量一般,但在Web组件花费了大量投入,如果把精力花在其他地方,那web发展能取得什么样的成就呢?
原文:https://dev.to/richharris/why-i-don-t-use-web-components-2cia
本文为 CSDN 翻译,转载请注明来源出处。