坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.3

前言

前端的东西很多,很杂,一会儿不学就有被淘汰的风险,之前学的Rust打包成webAssembly就是一种可能的趋势,而web component也是。

坏蛋Dan:rust基础学习--搭配webAssembly


参考文档

Web Components | MDN (mozilla.org)developer.mozilla.org/en-US/docs/Web/Web_Components


什么是Web Components

根据教程里的说法,Web Components是一套不同的技术(technologies),它允许我们创建可复用的自定义元素(elements)。我们可以把这些可复用的元素加到我们的app中使用。

你可能会觉得没必要,我们现在基于各种框架/库来开发,也是写组件,也可以复用。

但是你要知道这其中还是有区别的,说个不是很恰当的比喻:原生和非原生。

我们的框架/库,它们的组件需要经过“编译”之后才可以被浏览器认识,而Web Components则是浏览器直接支持的。

如果原生可以支持,那么就不需要“编译”,对开发和性能来说是一件好事。

当然,有一点需要特别注意:原生的并不一定就比非原生的好。


目的

至于目的是啥大家都知道,就是为了解决前端原生可复用性以及可管理性的问题。


组成

Web Components主要由一下三个技术点组成:

  • Custom elements:一组JS API集合允许我们自定义元素和它们的表现。
  • Shadow DOM:也是一组JS API,用来将我们封装好的Shadow DOM Tree加到某个独立于main document DOM渲染的元素上,并控制相关功能。通过这种方式,我们可以保持某个特性私有化(private),这就意味着我们不再需要担忧变量/样式污染等问题,因为是独立的。
  • HTML templatestemplate以及slot俩元素允许我们标记(makeup)不会展示与渲染页面的模板,它们可以被很多地方复用。

实现步骤

实现一个Web Components的基础方法大概有以下几个步骤:

  • 创建一个class用来定义我们的web component的功能
  • 使用CustomElementRegistry.define()来注册我们的自定义元素
  • 如果有需要,使用Element.attachShadow(),将你的Shadow DOM附加(attach)到你的自定义元素上。比如加一些子元素,事件等。
  • 如果有需要,使用template/slot俩元素定义一个HTML模板,然后将它们加入到你的Shadow DOM里。
  • 在你的app中使用这个自定义元素,哪里都可以。

使用自定义元素

创建一个自定义元素用于封装你想要的功能这一能力是Web Components标准里的一个关键特性。

我们将了解到如何使用自定义元素的API

高阶视图(High-level view)

web document中,自定义元素的控制器(controller)是CustomElementRegistry这个对象,它允许我们注册自定义元素到页面上,它会返回元素注册的目标的信息。

而具体注册的方法是CustomElementRegistry.define(),它接收一下几个参数:

  • 一个字符串代表这个自定义元素的名字,注意,规范是小写 + -符号连接各个词(kebab-case),具体可以看:https://html.spec.whatwg.org/#valid-custom-element-name
  • 一个class对象,它定义了你这个自定义元素的行为
  • Optional,选项,它包含了一个extends的属性用来指明你这个自定义元素想要继承于哪个原生元素,当然,非必要的。

自定义元素有两种类型:

  • Autonomous custom elements:自主性自定义元素,看名字就知道它是独立的(standalone),它不继承于任何的标准HTML元素。你可以直接将它的名字写到你的HTML文档里,比如``或者document.createElement('popup-info');
  • Customized built-in elements:自定义内置元素,它继承于某个元素的元素。在你注册的时候,你需要同时声明extends的原生元素。在使用它们的时候你就不能直接写它的名字了,得通过特殊的属性is,比如<p is="">或者document.createElement('p', { is: 'word-count' });

例子

我们先来看个自定义内置元素的例子:Simple word count web component (mdn.github.io)

我们继承了p元素,自定义了一个word-count的元素。

我们的class第一步就是super[3] ,表示我们想访问这个class constructor

HTMLParagraphElement[4]: 看名字就知道是P标签的元素类型,它继承于HTMLElement

然后我们获取当前节点的父节点的所有文本内容,根据空格分隔,计算的结果放到一个span里再插回去,这个时候插得用Shadow DOM的方式。

注意这里define得在WordCount声明之后。

另外如果你去log这个this的时候你会发现我们的dom都在一个Shadow root里面

img_shadow_root

然后我们再来看个独立自定义元素的例子:https://mdn.github.io/web-components-examples/popup-info-box-web-component/

代码很好懂,我们创建了一个自定义元素popup-info,给它加了一个icon和一个提示框,当鼠标移动到这个icon的时候,这个提示框的opacity变成1,也就是展示出来。

然后我们再来看下它在HTML中的样子


内部样式 vs 外部样式

上面的例子中,我们使用到了style元素,我们把css放到我们的Shadow root里面,实际上我们可以把它抽出来放到单独的css文件中,使用link外链入当前Shadow root里,这样就比较简洁,还有代码提示。

比如把样式放到style.css文件中。

不过有一点需要注意,因为link链入的代码不会堵塞渲染,所以初始化的时候可能存在无样式的闪烁(flash of unstyled content,也就是FOUC)。

许多现代浏览器对style从公共节点的克隆或具有相同文本的标签进行优化。让他们共享单个样式表,通过这种优化,外部和内部样式的性能应该是差不多的。


自定义内置元素

我们再来看个自定义内置元素的例子:https://mdn.github.io/web-components-examples/expanding-list-web-component/

代码也挺简单,就是先把ul里的li隐藏,然后给ul绑定点击事件showul,当点击的时候将隐藏的展示出来。

HTMLUListElement:和之前我们遇到的p一样,都是继承于HTMLElement,它是ul的类型。


使用生命周期回调(lifecycle callbacks)

在我们定义class的时候,我们可以使用别的回调,它们会在元素的不同生命周期时触发。

  • conntectedCallback:它会在元素每次被插入到document-connected element里的时候触发。节点移动的时候也会触发,也有可能在元素完全解析前触发。有一点需要注意,你的元素失去连接之后也有可能触发,所以你需要使用Node.isConnected 来确认是否还在连接中。
  • disconnectedCallback:当元素从文档DOM中失去连接时触发。
  • adoptedCallback:当元素被移动到新的文档中触发。
  • attributeChangedCallback:当自定义元素的其中一个属性变化/移除/新增时出触发。我们可以在静态方法observedAttributes指定要监听的属性

我们来搞个例子使用下这些生命周期回调:https://mdn.github.io/web-components-examples/life-cycle-callbacks/

我们定义了obervedAttributes,监听cl两个属性的变化。

然后我们往Shadow root里放俩没有内容的元素,然后监听这几个生命周期。其中connectedCallbackattributeChangedCallback回调中我们调用了updateStyle方法,这个方法会把前面创建的style标签里的内容覆盖为一段css

另外注意这个attributeChangedCallback有三个参数,第一个是属性名,第二个是旧数据,第三个是新的数据。

然后我们给三个button绑定了事件,一个是新增我们的自定义元素,一个是修改我们元素的lc属性,还有一个是移除元素。

我们触发第一个按钮,它将我们定义的custom-square添加到文档里,此时触发了三次生命周期回调

第一个自然就是connectedCallback,表示该元素被加到文档里了,后面两个则是元素属性添加时触发。

这个一目了然,就不多说了。

可以看到只触发了disconnectedCallback


转移器(Transpiler) vs 类(classes)[7]

需要注意class的兼容性,它不能被Babel 6或者Typescript编译目标是近代(或者叫传统(legacy))浏览器的情况正常识别。当然,Babel 6中可以搭配babel-plugin-transform-builtin-classes这个插件来处理。而对于typescript编译目标是近代浏览器这个问题建议是直接放弃兼容近代浏览器。

这里也引申一个Web Components兼容性问题,不支持近代浏览器。


第三方库[8]

前端的生态生命力是很磅礴的,所以我们并不需要去担心没有库或者工具之类的支持。

这里推荐几个Web Components的第三方库:

其中的Lit相信大家或多或少都有看到过名字,比如vite内置就支持这个Lit

问题指引

对于新技术来说,最大的问题就是接触的人少,进而导致有的问题可能搜不到相关的问题。

这种时候最好的方法就是去官方的github里提issue,不过保持提问题的好习惯,带上问题复现链接或者步骤以及简要的问题说明:

https://github.com/mdn/content/issues/new?template=page-report.yml&mdn-url=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FWeb_Components%2FUsing_custom_elements&metadata=%3C%21--+Do+not+make+changes+below+this+line+--%3E%0A%3Cdetails%3E%0A%3Csummary%3EPage+report+details%3C%2Fsummary%3E%0A%0A*+Folder%3A+%60en-us%2Fweb%2Fweb_components%2Fusing_custom_elements%60%0A*+MDN+URL%3A+https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FWeb_Components%2FUsing_custom_elements%0A*+GitHub+URL%3A+https%3A%2F%2Fgithub.com%2Fmdn%2Fcontent%2Fblob%2Fmain%2Ffiles%2Fen-us%2Fweb%2Fweb_components%2Fusing_custom_elements%2Findex.md%0A*+Last+commit%3A+https%3A%2F%2Fgithub.com%2Fmdn%2Fcontent%2Fcommit%2Fb01008f185ef0f6ca6603b02b71fb784c109e0ac%0A*+Document+last+modified%3A+2023-03-03T04%3A47%3A01.000Z%0A%0A%3C%2Fdetails%3Egithub.com/mdn/content/issues/new?template=page-report.yml&mdn-url=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FWeb_Components%2FUsing_custom_elements&metadata=%3C%21--+Do+not+make+changes+below+this+line+--%3E%0A%3Cdetails%3E%0A%3Csummary%3EPage+report+details%3C%2Fsummary%3E%0A%0A*+Folder%3A+%60en-us%2Fweb%2Fweb_components%2Fusing_custom_elements%60%0A*+MDN+URL%3A+https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FWeb_Components%2FUsing_custom_elements%0A*+GitHub+URL%3A+https%3A%2F%2Fgithub.com%2Fmdn%2Fcontent%2Fblob%2Fmain%2Ffiles%2Fen-us%2Fweb%2Fweb_components%2Fusing_custom_elements%2Findex.md%0A*+Last+commit%3A+https%3A%2F%2Fgithub.com%2Fmdn%2Fcontent%2Fcommit%2Fb01008f185ef0f6ca6603b02b71fb784c109e0ac%0A*+Document+last+modified%3A+2023-03-03T04%3A47%3A01.000Z%0A%0A%3C%2Fdetails%3E

当然,你也可以通过阅读源码的方式排查问题:https://github.com/mdn/content/blob/main/files/en-us/web/web_components/using_custom_elements/index.md?plain=1


使用Shadow DOM

Shadow DOM也是Web Copmonents的关键特性,它允许我们保留的标记结构(markup structure)、style以及行为等独立于当前页面的其它代码,这样就不会有冲突问题。

高阶视图

在我们学习之前,我们需要先了解DOM(Document Object Model)。当然,这玩意儿相信大家或多或少都有了解过,毕竟八股文的环境下,不去了解可能连第一轮面试都过不了。

简单地说,我们的HTML被浏览器的解析器时会生成的一棵树,这棵树就是DOM tree,它关联所有的node,也就是节点。当然,也不一定就是HTML,也有可能是其它标记语言。

比如

会被解析成以下这样

而我们的Shadow DOM可以被附加到常规的(regular)的DOM tree里,它的根节点就是Shadow root,我们可以往这个Shadow DOM Tree里加任何的元素。

Shadow DOM有以下几个专业术语(terminology)我们需要知道的:

  • Shadow host:被Shadow DOM依附的那个dom
  • Shadow treeShadow DOM里的DOM tree
  • Shadow boundaryShadow DOM结束,常规DOM开始的地方。
  • Shadow rootShadow DOM Tree的根节点。

你可以做和操作non-shadow DOM一样的事情,比如给Shadow DOM新增一个元素,修改元素属性等。

不过有一点需要特别注意,那就是我们的Shadow DOM相关的代码不会和它之外的代码有冲突,可以理解为做了一层封装。

Shadow DOM不是一种新的东西,这玩意儿浏览器早就支持了,它被用于封装元素的内部结构。

最直接的例子就是video,它自带一堆按钮等东西,但是我们使用的时候却只需要加上这个标签,给它设置几个属性即可,甚至属性都不需要设置,因为默认就有。


基础用发

我们可以使用Using shadow DOM - Web Components | MDN (mozilla.org)方法将我们的Shadow root依附到某个元素上。它接受一个对象作为参数option,我们前面的例子中就有用到,我们传入了{ mode: open },这个mode还有一个closed的值。

当然,在自定义元素的class里面直接用this.attachShadow即可。

open表示我们可以在主页面上下文(main page context)通过javascript访问Shadow DOM,比如:Element.shadowRoot属性

那么closed自然是不可以访问,此时的shadowRoot会变成null


例子

例子咱就不看了,前面的popup-info即可。


使用templatesslots

我们可以使用templateslot来创建灵活的模板,这样我们就可以动态插入shadow DOM了。

templates

template标签不会渲染到页面上,但是我们可以通过javascript抓到它。

我们来看个例子

这块代码的内容目前是不会渲染到页面上的,我们可以通过append将它“插入”到某个元素下


Web Components中使用templates

咱直接上例子

代码很好理解,就是通过id获取my-paragraph获取对应的dom,然后将它的内容放到我们的my-paragraph自定义元素里。

不过这里有一点需要注意,我们用的Node.cloneNode复制了这个节点,传入的参数true表示要深复制,整棵树都复制。这么做是为了避免多个自定义元素使用同一个模板导致数据覆盖的问题。

因为我们是用在Shadow DOM里的,所以我们甚至可以给这个template搞个style


slots

现在我们有了template,灵活复用性已经很不错了,但是还是不够灵活。

在面对只有部分内容不同但是大体上相同的场景还是不适用,虽然可以将我们的template进行小粒度化然后通过拼接的方式来灵活组合,但是这样就导致代码量以及需要维护的量也跟着上去了。

这个时候插槽slot就很有用了,我们可以把不同的地方设置为一个插槽,不同场景调用往里传不同的内容即可。

我们来修改下我们的代码

用法相信大家都很清楚了,这里就不多说了。


稍微复杂点的例子

在线链接:https://mdn.github.io/web-components-examples/element-details/

直接上代码

我们给element-details-template这个template搞了仨命名插槽,分别是element-namedescription、以及attributes

其实也不是很复杂,相信大家都用过vue,这种场景对大家来说就是小菜一碟。


引用

  1. CustomElementRegistry:包含自定义元素的功能,最常用的是CustomElementRegistry.define()方法,用于注册我们的自定义元素。
  2. Window.customElements:返回CustomElementRegistry对象的引用。
  3. Life cycle callbacks:生命周期回调:自定义函数在不同生命周期的时候会触发的hooks。有下面几个生命周期回调:
  • connectedCallback:当自定义元素第一次依附到document's DOM时触发。
  • disconnectedCallback:当自定义元素失去连接时触发。
  • adoptedCallback:当自定义元素被移动到别的文档时触发。
  • attributeChangedCallback:当自定义元素的属性发生变化时触发,包括新增和移除。
  • is :全局HTML属性,允许我们指定某个原生元素表现为我们自定义的内置元素。
  • css伪类(pseudo-classes):
  • :defined:匹配任何defined的元素,包括我们通过CustomElementRegistry.define()插入的自定义内置元素。
  • :host():选择shadow DOMshadow host。不过前提是作为函数参数的选择器和shadow hostDOM中的位置匹配得上。而如果不是函数,那么就是直接获取host
  • :host-context():同上,不过是处于作为函数参数的选择器里面的任何规则。
  • css伪元素(pseudo-elements):
  • ::part:选择任何带有part属性的shadow DOM
  • ::slotted:匹配任何插入到slot里的内容。
  • ShadowRoot:代表shadow DOM Tree的根节点
  • ShadowRoot 继承
  • Element.attachShadow():该方法可以将我们的自定义元素依附到某个文档元素上。
  • Element.shadowRoot:一个属性返回shadow root,如果modefalse,则返回null
  • Node相关:
  • Node.getRootNode():返回上下文对象的根节点,如果有shadow rootmodetrue,那么它也会被抓到。
  • Node.isConnected:这个属性返回一个布尔值表示这个节点是否还处于连接中
  • Event拓展:
  • Event.composed:如果一个事件会穿透shadow root的边界进入到non-shadow DOM里面就返回true
  • Event.composedPath:返回触发事件的路径,同样shadow DOM的都受mode控制捕获。
  • <template>:类型是HTMLTemplateElement。默认不会渲染,直到开发者用js去手动触发给它加到文档里。
  • ``:类型是HTMLSlotElement。你可以基于它设置占位符,再灵活覆盖占位符。
  • Element.assignedSlot:一个只读属性返回你这元素插入的插槽的引用。
  • Text.assignedSlot:同上,不过是textNode
  • Element.slot:返回依附于这个元素的shadow DOM slot的名字。
  • slotchange:当插入到插槽里的元素发生变化时被HTMLSlotElement触发。

例子

这里面有很多的例子:web-components-examples


浏览器兼容性

html.elements.template

api.ShadowRoot


总结

对于较大的项目来说,还是优先选择第三方框架比如vue、react等。

但如果你只是想搞个官网,静态页面等,还是很不错的。

参考

  1. ^Using custom elements https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements
  2. ^High-level View https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#high-level_view
  3. ^super https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super
  4. ^HTMLParagraphElement https://developer.mozilla.org/en-US/docs/Web/API/HTMLParagraphElement
  5. ^Internal vs external styles https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#internal_vs._external_styles
  6. ^customized built-in elements https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#customized_built-in_elements
  7. ^Transpiler vs classes https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#transpilers_vs._classes
  8. ^libraries https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#libraries
  9. ^Using Shadow DOM https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM
  10. ^Shadow DOM basic usage https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM#basic_usage
  11. ^Using templates and slots https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots
  12. ^the truth about templates https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots#the_truth_about_templates
  13. ^using templates in web components https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots#using_templates_with_web_components
  14. ^slots https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots#adding_flexibility_with_slots
  15. ^a more involved example https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots#a_more_involved_example
  16. ^Reference https://developer.mozilla.org/en-US/docs/Web/Web_Components#reference

编辑于 2023-03-05 23:26・IP 属地广东