Web Components

CustomElement v1规范发布之后, 在不使用框架的情况下创建Web组件变得更容易.
如需直接编写Web Components, 请参考:
https://zh.javascript.info/web-components
https://developers.google.com/web/fundamentals/web-components/customelements
用户使用Widget时只需导入一段打包好的JavaScript代码(或者ESM), 然后在对应的HTML位置插入组件即可.
WC提供了一种与框架无关的组件封装能力, 这是现存的MVVM框架无法做到的.
在用WC模仿Angular或Vue这类基于模板引擎的框架时, 可能会按照以下方式编写HTML, 可读性不强:
<my-app>
<my-nav></my-nav>
<logic-if condition="$some_condition">
<template slot="then">
<my-article></my-article>
</template>
<template slot="else">
<my-article></my-article>
</template>
</logic-if>
</my-app>
由于WC在技术演进方面的持续失败, 它似乎永远不可能取代MVVM框架的位置:
MVVM框架在功能, 开发人员体验, 迭代速度, 生态环境上都远远强于WC.
Web Components尤其缺乏像React一样的高阶组件功能, 使得代码重用成为问题:
  • Custom Elements必须在注册之后才能被重用.
    渲染时, 需要提供被包装组件的名称, 而被包装组件此时可能只是一个临时组件, 既没有注册也不应该注册.
  • DOM里的HTMLElement是不能通过new运算符实例化的, Custom Elements规范也缺乏对渲染方法的定义.
    这两点使得通用的高阶组件在Web Components里难以实现, 因为至少需要约定一个用于渲染的render方法.
    此外, 由于Web Components的组件承载了太多功能, 组件之间的嵌套关系很容易把问题变得很复杂.
Shadow DOM是Web Components技术栈下的一项 可选功能,
Shadow DOM的存在使Web Components与现有的其他技术有了决定性不同.
遗憾的是, Shadow DOM这项技术本身不是很有意义:
Web开发并不存在非得引入Shadow DOM才能解决的问题, 这项技术很大程度上是在解决不存在的问题.
目前看来, Shadow DOM的用途如下:
  • 阻断脚本对组件内部DOM的访问.
    我想不出除阻止用户脚本操作DOM以外的使用场景, 至少99.9%的网站不需要此项功能.
  • 阻断外部样式对组件内部DOM的影响.
    由于中型以上的项目根本不会使用类名以外的方式为元素设置样式(基于选择器的方案是难以维护的, 注定会陷入仅追加样式的窘境),
    这项功能显得非常多余.
对Shadow DOM节点树的称呼, 在很大程度上可以与Shadow DOM替换使用.
对Shadow Tree的根节点的称呼.
在此模式下, 可以让宿主环境的代码通过根元素的shadowRoot属性访问到Shadow DOM, 从而允许外部代码修改Shadow DOM里的元素属性.
在此模式下, 根元素的shadowRoot属性为null, 无法从外部访问(除非组件在创建时将attachShadow的返回值存储并暴露在外).
const originalAttachShadow = Element.prototype.attachShadow
Element.prototype.attachShadow = function () {
return originalAttachShadow.call(this, { mode: 'open' })
}
Chrome提供了一个 chrome.dom.openOrClosedShadowRoot 方法用于访问ShadowRoot.
在Firefox里, 可以直接通过访问元素的成员 element.openOrClosedShadowRoot 来访问ShadowRoot.
Slot插槽将Light DOM的元素填充(实际表现更像是映射或者链接)
到自定义元素的Shadow DOM里(插槽位于Shadow DOM内, 填充的元素则来自Light DOM).
被填充的元素在Light DOM里获得的样式在填充后仍然有效.
由于Shadow DOM隔离了宿主, 所以无法在外部通过CSS选择器为内部元素设置样式.
Web Components的样式通常需要通过CSS变量或组件本身的属性来间接设置样式.
Light DOM是与Shadow DOM相对的概念, 具体指代具有以下代码的组件:
// LitElement LightDOM
createRenderRoot() {
return this // 将内部元素暴露到文档级别, 而不是ShadowRoot里
}
// 对于一般CustomElement用例, 只需要用this替代this.shadowRoot
通过将host直接暴露为Light DOM, 从而支持通过CSS选择器为自定义元素设置样式.
来自Light DOM的样式会优先于Shadow DOM里通过 :host 伪元素设置的样式.
该方案的CSS选择器受到Shadow DOM的严重限制.
草案在Shadow Host上定义了一种 ::part() 伪元素, 允许在外部通过CSS选择器设置内部元素的样式.
::part() 无法通过伪元素继续匹配该元素的后代, 即 不能穿透多层Shadow Tree.
当Web组件内部嵌套其他Web组件时, 无法设置深层Web组件的样式.
以上问题进而衍生出一个新的属性exportparts, 从而允许导出深层的Web组件的part.
带有重命名的导出:
<style>
component::part(textspan) {
color: red;
}
</style>
<!-- Shadow Tree in component -->
<!-- 用exportparts公开更深层的part, 多个导出用逗号隔开 -->
<inner-component exportparts="innerspan: textspan"></inner-component>
<!-- Shadow Tree in inner-component -->
<span part="innerspan">红色</span>
<span part="textspan">不是红色</span>
示例2: 直接导出
<style>
component::part(innerspan) {
color: red;
}
</style>
<!-- 用exportparts公开更深层的part, 多个导出用逗号隔开 -->
<inner-component exportparts="innerspan"><</inner-component>
<!-- Shadow Tree in inner-component -->
<span part="innerspan">红色</span>
该方案仍然存在一个问题, 即如何 导出深层元素的全部属性, 有提案建议引入通配符来解决问题.
::part() 基本一样, 但对Shadow Tree具有穿透效果, 因此可以避免定义大量exportparts.