目录#
简介#
本文翻译自作者 Rich Harris 的 Why I don’t use web components
该文章引起了许多讨论,建议阅后前往原文评论区观摩 :)
前言#
作为我来到 dev.to 发表的第一篇文章,我觉得我可以和大家一起来讨论一下一个很nice,没有什么争议的话题: web components;
我写这篇文章主要是为了备忘,以便下次有人问我一些像 为什么对 web components 持怀疑态度 或者 Svelte 为什么不把编译到web components 作为默认选项 这类问题时,可以再拿出来参考一下。(实际上,根据 Custom Elements Everywhere 上svelte所得到的完美分数,足以证明它能够很好的兼容 web components)
以下所讨论的内容都不应该被当作是对 web components 所包含的艰苦工作的批评。我在这篇文章中可能犯了一些错误,欢迎在评论中更正。
同时我也不是在劝诫你不要使用 web components,它有适合它的使用场景,这里我只是在阐述我不使用它的原因。
1.渐进式的增强#
虽然这可能是一种越来越过时的观点,但我还是认为网站应当尽可能地减少对 Javascript 的依赖,而 Web components 在这方面与我的观点相悖,它依赖于Javascript。
这对于存在着内在交互逻辑的组件是没有什么问题的(比如一个定制化的日期选择组件 <cool-datepicker>
),但是对于一个导航栏组件来说,会有些问题。举例来说,我们有一个 <twitter-share>
标签,它内部封装了只对本组件有效的css以及构建一个 Twitter web intenet 链接的所有交互逻辑。我可以在 svelte 中构建这个组件,将会生成以下的 HTML 代码片段用于服务器渲染:
1 | <a target="_blank" noreferrer href="..." class="svelte-1jnfxx"> |
换句话说,在可访问性上,它等同于一个标准的 <a>
标签。
当 Javascript 启用时,它以渐进式的方式增强用户体验:打开一个小的 popup 形式的小窗口,而不是直接打开一个网页Tab。当然,Javascript 被禁用时,它也能降级到打开 Tab 的方式。
但如果我用 web components 来编写这个组件时,它会长这样;
1 | <twitter-share text="..." url="..." via="..."/> |
Javascript 被禁用或者出现报错导致代码不能正常运行时,又或者是在不支持 web components 的旧版本浏览器上时,这个标签会变得不能正常工作并且不可访问;
注:svelte 使用 class="svelte-1jnfxx"
来限制 css的作用域,以确保其仅作用于该组件,而无需借助 Shadow DOM 来实现此目的,这也引出了我的下一个观点:
2. CSS in, emmm… JS#
如果你想通过 Shadow DOM 来实现样式隔离,那么你需要把你的 CSS 放进 <style>
标签;这个需求的唯一的替代方案(至少在希望避免FOUC的情况下),则是将 CSS 通过字符串的形式放入 Javascript 模块中。
这与我们前文已经提到的性能优化建议背道而驰,这个建议可以总结为“请尽量减少JavaScript的使用”,CSS-in-js社区常常因为他们没有将CSS放到.css文件中这件事而受到批评,现在我们遇到了同样的情况。
在未来的几年里,我们可能通过使用由 Constructable Stylesheets 构建的 CSS Modules 来解决这个问题,我们也可能通过用 ::theme
和 ::part
伪类来定义 Shadow DOM 内元素样式。但是这些方案也不是没有问题的。
3. 宿主环境的疲乏#
在撰写本文时,Chromium Bug展示页上有61,000个未解决的问题,反映出构建现代Web浏览器的巨大复杂性。
每次我们向平台添加新功能时,都会增加这种复杂性,为bug创造新的土壤,并使得Chromium出现新的竞争对手的可能性越来越小。
鼓励开发人员学习这些新功能会给开发人员带来了极大的挑战(其中一些功能(例如HTML导入或原始的Custom Elements规范,永远不会在Google外部流行并最终被再次删除)。
4. Polyfills#
如果您想要支持所有的浏览器,那么寄希望于polyfills似乎是不太可能的。
在 Constructable Stylesheets 这个由谷歌职员(嗨,Jason!)所编写的文章里,完全没有提到这是一个仅在 Chrome 有用的 feature (三个 spec editor 都是谷歌人。Webkit似乎对设计的某些方面存有疑问。)
5. Composition#
组件能够控制何时(或是否)呈现其 slot
内容是很有用的。假设我们想要使用
1 | <p>Toggle the section for more info:</p> |
Surprise! 即使你还没有打开这个页面,但浏览器已经请求了 more-info.html
网页,以及它所链接到的任何图像和资源。
这是因为slot的内容在自定义元素中会优先被渲染。事实证明,大多数情况下,你都希望将 slot 的内容延迟呈现。Svelte v2采用了 eager model,以便与web标准保持一致,但结果证明,它是导致我们无法创建一个与React Router等价的Router的主要原因。在Svelte v3中,我们放弃了自定义元素组合模型,并且不会再回头。
不幸的是,这只是DOM的一个基本特性。这就把我们带到了下面这另一个困境……
6. props 和 attributes 令人迷惑#
props 和 attributes 本质上是同一个东西,对吧?
1 | const button = document.createElement('button'); |
我是说,大多数情况下:
1 | typeof button.disabled; // 'boolean' |
然后我发现还有一些name是不匹配的……
1 | div = document.createElement('div'); |
…而这些似乎根本不匹配:
1 | input = document.createElement('input'); |
但是我们可以容忍这些奇怪的部分,因为在字符串格式(HTML)和DOM之间的转换过程中肯定会损失一些东西。这些损失的数量是有限的,并且有文档记录,所以只要有足够的时间和耐心,我们至少可以了解它们。
Web components 改变了这一点。不仅不再有任何关于 attributes 和 props 之间关系的确定性,而且作为一个Web components 的作者,你(大概?)应该同时支持这两者。这意味着你将会看到这样的情况:
1 | class MyThing extends HTMLElement { |
有时您会看到程序以另一种方式进行,即调用属性访问器的 attributeChangedCallback
。不管怎样,这对于人的现有认知都是极具毁灭性的。
相反,框架有一种简单而明确的方式将数据传递给组件。
7. 设计缺陷#
有一点比较模糊,让我感到奇怪的是,attributeChangedCallback
只是元素实例上的一个方法。你可以这么做:
1 | const element = document.querySelector('my-thing'); |
没有 attribute 发生改变,但是它表现得就像它改变了一样。当然,JavaScript总是提供了很多恶作剧的机会,但是当我看到这样的实现细节时,我总是觉得他们是在试图告诉我们这样的设计不太正确。
8. DOM 这个东西不太好#
我们已经证明了DOM不是好的玩意。但是对于构建交互式应用程序来说我们不得不使用它,这是一个多么尴尬的情况,怎么说都不为过。
回到几个月前,我写了一篇名为 Write less code 的文章,旨在说明Svelte如何使你能比React和Vue等框架更有效地构建组件。但是我没有将它与DOM进行比较。我想我应该去比较一下了。
翻新一下,这里是一个简单的 <Adder a={1} b={2}/>
组件:
1 | <script> |
这就是完整的实现,我们再看看在 web component 中应该怎么做呢:
1 | class Adder extends HTMLElement { |
嗯…
还要注意,如果同时更改 a 和 b,将导致两个独立的更新。而框架通常不会遇到这个问题。
9. Global namespace#
我们不需要过多地考虑这个问题; 可以这样说,使用单一共享的namespace的危险,人们在一段时间以前就很好地认识到了。
10. 这些都是已经被解决了的问题#
最令人沮丧的是,我们已经有了非常好的组件模型。我们仍然在学习,但是通过以面向组件的方式操作DOM来保持视图与某些状态的同步的基本问题已经解决多年了。
然而,我们正在向平台添加新功能,只是为了让web组件与我们在userland中已经实现的功能对等。
在资源有限的情况下,花在一项任务上的时间意味着没有办法花时间在另一项任务上。尽管大多数开发人员对 web component 漠不关心,但 web component 已经消耗了开发人员大量的精力。如果把这些精力花在别的地方,互联网又可能会取得什么样的突破呢?
完。