前端的东西很多,很杂,一会儿不学就有被淘汰的风险,之前学的Rust
打包成webAssembly
就是一种可能的趋势,而web component
也是。
Web Components | MDN (mozilla.org)developer.mozilla.org/en-US/docs/Web/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 templates
:template
以及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-nameclass
对象,它定义了你这个自定义元素的行为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
中的样子
上面的例子中,我们使用到了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
,监听c
和l
两个属性的变化。
然后我们往Shadow root
里放俩没有内容的元素,然后监听这几个生命周期。其中connectedCallback
和attributeChangedCallback
回调中我们调用了updateStyle
方法,这个方法会把前面创建的style
标签里的内容覆盖为一段css
。
另外注意这个attributeChangedCallback
有三个参数,第一个是属性名,第二个是旧数据,第三个是新的数据。
然后我们给三个button
绑定了事件,一个是新增我们的自定义元素,一个是修改我们元素的l
和c
属性,还有一个是移除元素。
我们触发第一个按钮,它将我们定义的custom-square
添加到文档里,此时触发了三次生命周期回调
第一个自然就是connectedCallback
,表示该元素被加到文档里了,后面两个则是元素属性添加时触发。
这个一目了然,就不多说了。
可以看到只触发了disconnectedCallback
。
Transpiler
) vs 类(classes
)[7]需要注意class
的兼容性,它不能被Babel 6
或者Typescript
编译目标是近代(或者叫传统(legacy
))浏览器的情况正常识别。当然,Babel 6
中可以搭配babel-plugin-transform-builtin-classes
这个插件来处理。而对于typescript
编译目标是近代浏览器这个问题建议是直接放弃兼容近代浏览器。
这里也引申一个Web Components
兼容性问题,不支持近代浏览器。
前端的生态生命力是很磅礴的,所以我们并不需要去担心没有库或者工具之类的支持。
这里推荐几个Web Components
的第三方库:
FASTElement
snuggsi
X-Tag
Slim.js
Lit
Smart
其中的Lit
相信大家或多或少都有看到过名字,比如vite
内置就支持这个Lit
。
对于新技术来说,最大的问题就是接触的人少,进而导致有的问题可能搜不到相关的问题。
这种时候最好的方法就是去官方的github
里提issue
,不过保持提问题的好习惯,带上问题复现链接或者步骤以及简要的问题说明:
当然,你也可以通过阅读源码的方式排查问题: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 tree
:Shadow DOM
里的DOM tree
。Shadow boundary
:Shadow DOM
结束,常规DOM
开始的地方。Shadow root
:Shadow 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
即可。
templates
和slots
我们可以使用template
和slot
来创建灵活的模板,这样我们就可以动态插入shadow DOM
了。
template
标签不会渲染到页面上,但是我们可以通过javascript
抓到它。
我们来看个例子
这块代码的内容目前是不会渲染到页面上的,我们可以通过append
将它“插入”到某个元素下
Web Components
中使用templates
咱直接上例子
代码很好理解,就是通过id
获取my-paragraph
获取对应的dom
,然后将它的内容放到我们的my-paragraph
自定义元素里。
不过这里有一点需要注意,我们用的Node.cloneNode
复制了这个节点,传入的参数true
表示要深复制,整棵树都复制。这么做是为了避免多个自定义元素使用同一个模板导致数据覆盖的问题。
因为我们是用在Shadow DOM
里的,所以我们甚至可以给这个template
搞个style
。
现在我们有了template
,灵活复用性已经很不错了,但是还是不够灵活。
在面对只有部分内容不同但是大体上相同的场景还是不适用,虽然可以将我们的template
进行小粒度化然后通过拼接的方式来灵活组合,但是这样就导致代码量以及需要维护的量也跟着上去了。
这个时候插槽slot
就很有用了,我们可以把不同的地方设置为一个插槽,不同场景调用往里传不同的内容即可。
我们来修改下我们的代码
用法相信大家都很清楚了,这里就不多说了。
在线链接:https://mdn.github.io/web-components-examples/element-details/
直接上代码
我们给element-details-template
这个template
搞了仨命名插槽,分别是element-name
、 description
、以及attributes
。
其实也不是很复杂,相信大家都用过vue
,这种场景对大家来说就是小菜一碟。
CustomElementRegistry
:包含自定义元素的功能,最常用的是CustomElementRegistry.define()
方法,用于注册我们的自定义元素。Window.customElements
:返回CustomElementRegistry
对象的引用。hooks
。有下面几个生命周期回调:connectedCallback
:当自定义元素第一次依附到document's DOM
时触发。disconnectedCallback
:当自定义元素失去连接时触发。adoptedCallback
:当自定义元素被移动到别的文档时触发。attributeChangedCallback
:当自定义元素的属性发生变化时触发,包括新增和移除。is
:全局HTML
属性,允许我们指定某个原生元素表现为我们自定义的内置元素。css
伪类(pseudo-classes
)::defined
:匹配任何defined
的元素,包括我们通过CustomElementRegistry.define()
插入的自定义内置元素。:host()
:选择shadow DOM
的shadow host
。不过前提是作为函数参数的选择器和shadow host
在DOM
中的位置匹配得上。而如果不是函数,那么就是直接获取host
。:host-context()
:同上,不过是处于作为函数参数的选择器里面的任何规则。css
伪元素(pseudo-elements
):::part
:选择任何带有part
属性的shadow DOM
。::slotted
:匹配任何插入到slot
里的内容。ShadowRoot
:代表shadow DOM Tree
的根节点ShadowRoot
继承Element.attachShadow()
:该方法可以将我们的自定义元素依附到某个文档元素上。Element.shadowRoot
:一个属性返回shadow root
,如果mode
为false
,则返回null
。Node
相关:Node.getRootNode()
:返回上下文对象的根节点,如果有shadow root
的mode
是true
,那么它也会被抓到。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
对于较大的项目来说,还是优先选择第三方框架比如vue、react
等。
但如果你只是想搞个官网,静态页面等,还是很不错的。
编辑于 2023-03-05 23:26・IP 属地广东