读完此文你将系统了解原生web组件组成以及它们之间的相互作用,并且对日常开发中遇到的问题会有更深层次的理解。
Web Components主要解决代码复用及组件封装问题,其包括三个主要部分:
- Custom elements(自定义元素):允许定义custom elements及其行为,用户可在界面中按需使用它们。
- Shadow DOM(影子DOM):用于将封装的“影子”DOM树附加到元素上(与主文档DOM分开渲染),并控制其关联的功能。通过这种方式,可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
- HTML templates(HTML模板):
和
元素可以用来编写不在渲染页面中显示的标记模板,它们可作为自定义元素结构的基础被多次重用。
下面将详细叙述这三部分的使用及其相互作用。
Custom Elements(自定义元素)
顾名思义,可以自己定义的元素标签,类似于在vue,react中我们为了方便组件的复用,而把某个常用功能单独写成一个组件形式,后续可以直接调用该组件。不同的是,custom elements是一个封装好的html标签,他可以基于现有标签进行功能扩展,也可以单独定义标签直接调用,并且结合shadow DOM
来使用可以做到减少组件中深层嵌套的标签,使DOM更加简洁。
自定义元素的定义api:Window.customElements.define()
语法:
customElements.define('custom-element-name', customElementsClass, [extends]);
-
custom-element-name
:必填,所创建的自定义元素名称,需符合DOMString
标准的字符串。注意!custom elements 的名称不能是单个单词
,且其中必须要有短横线
(-)。 -
customElementsClass
:必填,constructor,通常是用于定义元素行为的类
。 -
extends
:可选参数,一个包含extends
属性的配置对象。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。
customElements
是Window对象上的一个只读属性,接口返回一个 CustomElementRegistry
对象的引用,可用于注册新的 custom elements。
举个例子:下面注册了一个通用
标签
- 注册custom elements
// 为元素创建一个class
class myFooter extends HTMLElement {
constructor() {
// 必须先调用super方法,类的构造函数constructor总是先调用super()来建立正确的原型链继承关系。
self = super();
const imgAttr = this.getAttribute('img')
const textAttr = this.getAttribute('text')
const style = document.createElement('style');
style.textContent = ` div {text-align:center;margin: 0 auto; } `;
const imgNode = document.createElement('img')
imgNode.setAttribute('src', imgAttr)
const textNode = document.createElement('div')
textNode.innerHTML = textAttr
const rootNode = document.createElement('div')
rootNode.appendChild(imgNode)
rootNode.appendChild(textNode)
rootNode.appendChild(style);
this.appendChild(rootNode)
}
}
// 定义自定义元素标签
customElements.define('my-footer', myFooter);
- 调用自定义标签
-
效果
Custom Elements的分类
自定义元素分为两类:
-
Autonomous custom elements
自主定义元素 -
Customized built-in elements
自定义内置元素
Autonomous custom elements 自主定义元素
如同上文定义的一个
标签一样,custom elements不继承其他内置的HTML元素,你可以直接把它们写成HTML标签的形式在页面上使用,如
,或者通过document.createElement("my-footer")创建元素。Autonomous custom elements
的类定义总是继承自HTMLElement
.
Customized built-in elements 自定义内置元素
Customized built-in elements
继承自基本的HTML元素。在创建时,必须指定所需扩展的元素,使用时,需要先写出基本的元素标签,并通过 is
属性指定custom elements的名称。例如, 或者
document.createElement("p", { is: "word-count" })
。
// 自定义内置元素
class moneyFormat extends HTMLParagraphElement {
constructor() {
self = super();
const money = this.textContent
this.textContent = `${money / 10000}万`
}
}
customElements.define('money-format', moneyFormat, { extends: 'p'});
// 你可以正常使用标签,也可以通过is属性来指定一个custom elements的名称
100000
// 在页面上显示出来的就是100000
100000
// 在页面上显示出来的就是10万
// 通过动态创建自定义内置元素
cosnt moneyFormat = document.createElement('p', { is: 'money-format' })
这里的真正不同点在于元素定义类继承的是HTMLParagraphElement
接口(不同的元素继承的接口都不一样,继承的是
HTMLParagraphElement
,
继承的的是HTMLUListElement
),而不是HTMLElement
。所以它拥有元素所有的特性,以及在此基础上我们定义的功能,这是与独立元素(standalone element)不同之处。这也是为什么我们称它为
customized built-in元素
,而不是一个autonomous元素
。
生命周期回调函数
生命周期 | 调用时机 |
---|---|
constructor | 创建元素的实例时调用,用于初始化状态、设置事件侦听器或创建影子dom |
connectedCallback | 当 custom elements首次被插入文档DOM时 |
disconnectedCallback | 当 custom elements从文档DOM中删除时 |
adoptedCallback | 当 custom elements被移动到新的文档时(document.adoptNode方法修改元素ownerDocument时触发) |
attributeChangedCallback(name, oldValue, newValue) | 当 custom element增加、删除、修改自身属性时,attributeChangedCallback()回调函数会执行 |
static get observedAttributes() {return ['attribute']; } | 如果需要在元素属性变化后,触发 attributeChangedCallback()回调函数,则需要监听这个属性 |
!注意:想要attributeChangedCallback
生效,必须设置observedAttributes
来返回该标签需要监听哪些属性的改变,两者需要结合使用。
下面示例展示了:定义自定义元素加入文档中、修改自定义元素的属性以及从文档中移除自定义属性的生命周期触发时机
定义自定义元素my-footer
以及添加绑定事件触发自定义元素的生命周期函数
// 自定义元素生命周期展示
class myFooter extends HTMLElement {
constructor() {
self = super();
const style = document.createElement('style');
style.textContent = `
div {text-align:center;margin: 0 auto; }
`;
document.body.appendChild(style);
this.updateText()
}
// 监听自定义元素的属性:text,发生改变时会触发 attributeChangedCallback 函数
static get observedAttributes() {
return ['text'];
}
connectedCallback() { console.log('【connectedCallback】Custom element added to page.'); }
disconnectedCallback() { console.log('【disconnectedCallback】Custom element removed from page.'); }
adoptedCallback() { console.log('【adoptedCallback】Custom element moved to new page.'); }
attributeChangedCallback(name, oldValue, newValue) {
console.log('【attributeChangedCallback】', name, oldValue, newValue)
console.log('【attributeChangedCallback】Custom element attributes changed.');
this.updateText()
}
// 更新自定义元素内容
updateText() {
const img = this.getAttribute('img')
const text = this.getAttribute('text')
this.innerHTML = `${text}`
}
}
// 定义自定义元素:my-footer
customElements.define('my-footer', myFooter);
const doc = document
const myFooterEle = doc.getElementById('my-footer')
const changeAttrBtn = doc.getElementById('change-attr-btn')
const removeAttrBtn = doc.getElementById('remove-attr-btn')
changeAttrBtn.onclick = function () {
myFooterEle.setAttribute('text', 'change footer')
}
removeAttrBtn.onclick = function () {
doc.body.removeChild(myFooterEle)
}
刷新页面:控制台输出
【attributeChangedCallback】 text null default footer
【attributeChangedCallback】Custom element attributes changed.
【connectedCallback】Custom element added to page.
点击changeAttrBtn
按钮:
【attributeChangedCallback】 text default footer change footer
【attributeChangedCallback】Custom element attributes changed.
点击removeAttrBtn
按钮:
【disconnectedCallback】Custom element removed from page.
css伪类
:defined
:defined
表示任何已定义的元素,包括任何浏览器内置的标准元素以及已成功定义的自定义元素 (例如通过 CustomElementRegistry.define()
方法定义的元素)。
/* 选择所有已定义的元素 */
:defined {
font-style: italic;
}
/* 选择指定自定义元素的任何实例 */
my-footer:defined {
display: block;
}
/* 在你有一个复杂的自定义元素需要一段时间才能加载到页面中时非常有用 —— 你可能想要隐藏元素的实例直到定义完成为止,这样你就不会在页面上出现一些难看的元素 */
my-footer:not(:defined) {
display: none;
}
改进
- 自定义元素都要动态通过JS生成DOM,很繁琐,并且不直观,针对这个问题,可以使用
HTML templates
中的和
。元素 - 每个自定义标签下面都嵌套一堆DOM元素,如下,十分冗余,并没有真正减负;其次,没有达到真正HTML和CSS封装的目的,容易受主文档的影响,针对这个问题,可以使用
Shadow DOM(影子DOM)
。
default footer
兼容性
可以看到主流浏览器对customElements接口
和Custom Elements标签
都兼容,IE不兼容,但可以通过polyfills去兼容(详情文末),并且Customized built-in elements
自定义内置元素兼容性不佳,部分浏览器不兼容。
Shadow DOM(影子DOM)
Shadow DOM
主要解决了 DOM 树的封装问题。Shadow DOM
允许在文档(document)渲染时插入一棵DOM元素子树,但是这棵子树不在主DOM树中,它与文档的主 DOM 树分开渲染。
什么是Shadow DOM
Shadow DOM
重要的特性就是封装性,它可以将DOM结构、css样式和行为隐藏起来,并与页面上的其他代码相隔开来,保证不同的部分不会混在一起,使代码更加干净整洁。Shadow DOM允许将隐藏的 DOM 树附加到常规的 DOM 树中(被附加的这个常规的树的节点叫shadow host
),它以 shadow root
节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样。你可以像操作常规DOM一样操作Shadow DOM,不同的是Shadow DOM内部的元素始终不会影响到它外部的元素,这为封装提供了便利。
Shadow DOM在哪里
Shadow DOM离我们其实并不遥远,平时我们在浏览器devtool工具里面看不到是因为我们没有开启显示shadow dom,打开方式:浏览器打开开发者调试工具-右上角“设置”图标-Preference-Elements-勾选“show user agent shadow DOM”
文档结构:
devtools中的elements表现:
可以看到,和
标签下挂载着一个
shadow-root
,但在常规调试工具中,是看不到里面的DOM结构的,看到的是代码中所写的样子。这里shadow-root下的DOM就是Shadow DOM。Shadow DOM里面元素以及样式不会影响外部,这也是封装的意义所在。(也可以看到video里面的Shadow DOM都带了pseudo
属性,这样我们就可以通过伪类::-webkit-media-controls
去改变video的样式)
Shadow DOM与常规DOM的关系:
Document Tree
|
Shadow host
------------------ 边界
|
Shadow root
|
Shadow Tree
- Shadow host:一个常规 DOM节点,Shadow DOM 会被附加到这个节点上。
- Shadow root:Shadow tree 的根节点。
- Shadow boundary:Shadow DOM结束的地方,也是常规 DOM 开始的地方。
- Shadow tree:Shadow DOM 内部的DOM树。
创建Shadow DOM
Element.attachShadow()
方法给指定的元素挂载一个Shadow DOM,并且返回对 ShadowRoot 的引用。
不是每一种类型的元素都可以附加到shadow root(影子根)下面。出于安全考虑,一些元素不能使用 shadow DOM(例如),以及许多其他的元素。
基本语法:var shadowroot = element.attachShadow(shadowRootInit
参数:
shadowRootInit(Object):
- mode:指定 Shadow DOM 树封装模式的字符串
- open: 可从外部访问shadow root元素的根节点,如
Element.shadowRoot
- closed: 不可以从外部获取 Shadow DOM,
Element.shadowRoot
返回null
- open: 可从外部访问shadow root元素的根节点,如
- delegatesFocus: 焦点委托
- 一个布尔值, 当设置为 true 时, 指定减轻自定义元素的聚焦性能问题行为.
- 当shadow DOM中不可聚焦的部分被点击时, 让第一个可聚焦的部分成为焦点, 并且shadow host(影子主机)将提供所有可用的 :focus 样式.
返回值:返回一个 ShadowRoot 对象或者 null。
提示:也可以选择host.createShadowRoot()
的方法创建Shadow Root挂载Shadow Tree
应用
结合custom elements,创建一个挂载Shadow DOM的自定义元素
class myFooter extends HTMLElement {
constructor() {
// 必须先调用super方法,类的构造函数constructor总是先调用super()来建立正确的原型链继承关系。
self = super();
// 自定义元素挂载一个Shadow DOM
this._shadowRoot = this.attachShadow({ mode: 'open' })
const style = document.createElement('style');
style.textContent = `img,div {text-align:center;margin: 0 auto;display: block};`;
const img = this.getAttribute('img')
const text = this.getAttribute('text')
const imgNode = document.createElement('img')
imgNode.setAttribute('src', img)
const textNode = document.createElement('div')
textNode.textContent = text
this._shadowRoot.appendChild(imgNode);
this._shadowRoot.appendChild(textNode);
this._shadowRoot.appendChild(style);
}
}
customElements.define('my-footer', myFooter);
调用:
DOM结构:
可以看到
下多了一个#shadow-root
,展开#shadow-root
才看到具体的shadow tree里面的结构,并且shadow dom中定义的样式不会影响主DOM中样式,当把调试工具中的勾选去掉“show user agent shadow DOM”后,在文档上就看不到
内具体的DOM树了,这就跟标签一样的表现形式了。
因为这里设置了mode:'open'
,所以可以在外面获取shadow root下的根节点。
console.log(document.getElementById('my-footer').shadowRoot)
// 控制台打印:
#shadow-root(open)
default footer
当mode: 'closed'
时,则外层无法获取Shadow DOM,标签就是设置了closed属性
console.log(document.getElementById('my-footer').shadowRoot) // null
从组件的可扩展性与灵活些来说,建议设置open属性,方便使用者进行修改扩展。
- mode="open": Ele.shadowRoot = #shadow-root(open)....
- mode="closed": Ele.shadowRoot = null
Event.composed && Event.composedPath()
属性/接口 | 返回值 | 作用 |
---|---|---|
Event.composed | Boolean | 若返回值为true,表明当事件到达 shadow DOM 的根节点(也就是 shadow DOM 中事件开始传播的第一个节点)时,事件可以从 shadow DOM 传递到一般 DOM。当然,事件要具有可传播性,即该事件的 bubbles 属性必须为 true。如果属性值为 false,那么事件将不会跨越 shadow DOM 的边界传播。 |
Event.composedPath() | 一个 EventTarget对象数组,表示将在其上调用事件侦听器的对象。 | 返回事件路径,如果影子根节点被创建并且ShadowRoot.mode是关闭的,那么该路径不包括影子树中的节点。 |
对文档设置事件监听
document.querySelector('html').addEventListener('click', function (e) {
console.log(e.bubbles)
console.log(e.composed);
console.log(e.composedPath());
}, false);
点击
中的标签,得到如下结论:
- 无论
ShadowRoot.mode
是open或closed,e.composed
都返回true,因为click事件
始终能跨越阴影边界传播。 - 不同在于
e.composedPath()
:-
ShadowRoot.mode=open
时,e.composedPath()
返回[img, document-fragment, my-footer#my-footer, body, html, document, Window]
,事件能到达Shadow DOM里面的元素 -
ShadowRoot.mode=closed
时,e.composedPath()
返回[my-footer#my-footer, body, html, document, Window]
,事件不能到达Shadow DOM中,监听器只会捕获到
元素本身
-
当一个事件从 Shadow DOM 冒泡时,它的target会被调整以保持 Shadow DOM 提供的封装。也就是说,事件被重新定位,使其看起来像是来自组件而不是影子 DOM 中的内部元素。有些事件甚至不会从 shadow DOM 传播出去。
以下为能跨越阴影边界的事件:
- Focus Events: blur, focus, focusin, focusout
- Mouse Events: click, dblclick, mousedown, mouseenter, mousemove, etc.
- Wheel Events: wheel
- Input Events: beforeinput, input
- Keyboard Events: keydown, keyup
- Composition Events: compositionstart, compositionupdate, compositionend
- DragEvent: dragstart, drag, dragend, drop, etc.
有些情况下事件绑定不进行重定向而直接被干掉,以下事件会被阻塞到根节点且不会被原有 DOM 结构监听,被阻塞的事件:
- abort
- error
- select
- change
- load
- reset
- resize
- scroll
可以参考以下实例解释:
class inputItem extends HTMLElement {
constructor() {
self = super();
this._shadowRoot = this.attachShadow({ mode: 'open' })
const text = this.getAttribute('text')
const textNode = document.createElement('div')
textNode.textContent = text
const inputNode = document.createElement('input')
inputNode.value = 'I am shadow dom text'
this._shadowRoot.appendChild(textNode);
this._shadowRoot.appendChild(inputNode);
}
}
customElements.define('input-item', inputItem);
document.addEventListener('change', function (e) {
console.log('[change event target]', e.target);
console.log('[change event bubbles]', e.bubbles)
console.log('[change event composed]', e.composed);
console.log('[change event composedPath]', e.composedPath());
});
document.addEventListener('click', function (e) {
console.log('[click event target]', e.target)
console.log('[click event bubbles]', e.bubbles)
console.log('[click event composed]', e.composed);
console.log('[click event composedPath]', e.composedPath());
});
当点击正常input标签并修改value的值
,事件冒泡到document,click和change事件能够成功被监听。可以看到change事件中composed返回false,devtool中开启“show Shadow-root”的话可以看到其实input标签下也是挂载了一个Shadow DOM,所以change事件不能从Shadow DOM中传递回一般的DOM,input是一个封装好的元素,保证了其封装性,change事件的target就是input元素本身。
[click event target] …
[click event bubbles] true
[click event composed] true
[click event composedPath] (5) [input#normal-text, body, html, document, Window]
[change event target] …
[change event bubbles] true
[change event composed] false
[change event composedPath] (5) [input#normal-text, body, html, document, Window]
当点击Shadow DOM中的input并修改值
,change事件冒泡到Shadow Root就会停止向上,所以绑定在document上的事件不会被触发,只有click事件能冒泡到document被触发,并且控制台上打印target的是宿主对象host,即
元素。这是因为影子节点上的事件必须重定向,否则这将破坏封装性。如果事件继续指向 #shadow-root 里面的元素,那么任何人都可以在 Shadow DOM 里破坏其内部结构,这就违背了其封装性的初衷。
[click event target] …
[click event bubbles] true
[click event composed] true
[click event composedPath] (7) [input, document-fragment, input-item, body, html, document, Window]
使用自定义事件
在影子树中的内部节点上触发的自定义DOM事件
不会冒泡出影子边界,除非该事件是使用 composed: true
标志创建的
使用 new Event()
new Event(eventName, eventInit
eventInit(Object),可选
- "bubbles",可选,Boolean类型,默认值为 false,表示该事件是否冒泡。
- "cancelable",可选,Boolean类型,默认值为 false, 表示该事件能否被取消。
- "composed",可选,Boolean类型,默认值为 false,指示事件是否会在影子DOM根节点之外触发侦听器。
class myFooter extends HTMLElement {
constructor() {
self = super();
this._shadowRoot = this.attachShadow({ mode: 'open' })
// 省略中间代码...
this._shadowRoot.addEventListener('click', this._changeText.bind(this))
}
_changeText() {
// 向一个指定的事件目标派发一个事件
this._shadowRoot.dispatchEvent(new Event('text-change', { bubbles: true, composed: true }));
}
}
customElements.define('my-footer', myFooter);
document.getElementById('my-footer').addEventListener('text-change', function(e) {
// 当事件定义参数不是`composed: true`时不会触发该事件
// 当事件定义参数`composed: true`时,输出:【text-change event】 Event {isTrusted: false, type: "text-change", target: my-footer#my-footer, currentTarget: my-footer#my-footer, eventPhase: 2, …}
console.log('【text-change event】', e)
});
使用 new CustomEvent()
相比于new Event()
,new CustomEvent()
可以自定义派发的数据。
new CustomEvent(eventName, eventInit
eventInit(Object)可选
- "bubbles",可选,Boolean类型,默认值为 false,表示该事件是否冒泡。
- "cancelable",可选,Boolean类型,默认值为 false, 表示该事件能否被取消。
- "detail",可选,any类型,默认值为 null,当事件初始化时传递的数据
class myFooter extends HTMLElement {
constructor() {
self = super();
this._shadowRoot = this.attachShadow({ mode: 'open' })
// 省略中间代码...
this._shadowRoot.addEventListener('click', this._changeText.bind(this))
}
_changeText() {
// 这里不在this._shadowRoot上派发,而是在上派发,因为CustomEvent事件默认composed是false,所以监听事件不会触发
this.dispatchEvent(new CustomEvent('text-change', { detail: { text: 'change text' } }));
}
}
customElements.define('my-footer', myFooter);
document.getElementById('my-footer').addEventListener('text-change', function(e) {
console.log('【text-change event】', e)
});
输出:
> CustomEvent {isTrusted: false, detail: null, type: "text-change", target: my-footer#my-footer, currentTarget: my-footer#my-footer, …}
bubbles: false
cancelBubble: false
cancelable: false
composed: false
currentTarget: null
defaultPrevented: false
detail: {text: "change text"} => 派发过来的数据
eventPhase: 0
isTrusted: false
path: (5) [my-footer#my-footer, body, html, document, Window]
returnValue: true
srcElement: my-footer#my-footer
target: my-footer#my-footer
timeStamp: 1882.7999997138977
type: "text-change"
__proto__: CustomEvent
css伪类
只能在Shadow DOM内使用,在之外使用时,没有任何效果。
-
:host
:选择包含其自定义元素内部的shadow DOM的根元素。 -
:host()
:选择包含使用这段 CSS 的 Shadow DOM 的影子宿主,只能选择host元素 -
:host-context()
:选择shadow DOM 中shadow host,这个伪类内可以写关于该shadow host的CSS规则。 在DOM 层级中,括号中的选择器参数必须和shadow host 的祖先相匹配
。可以用于主体化定制
,典型的使用方法是后代选择器表达式,例如只选择在内的自定义元素的实例。
/* 选择一个 shadow root host */
:host {
font-size: 16px;
}
/* 选择阴影根元素,仅当它与选择器参数匹配 */
:host(#my-footer) {
font-weight: bold;
}
/* 选择阴影根元素,仅当它与选择器参数匹配 */
:host(my-footer) {
color: green;
}
/* host元素下shadow dom */
:host(my-footer) #my-footer-text{
color: red;
}
/* 选择了一个 shadow root host, 当且仅当这个 shadow root host 是括号中选择器参数(h1)的后代 */
:host-context(h1) {
color: blue;
}
如果你想使用者可以从外部修改自定义元素的样式,那么开发者可以在自定义元素内部预埋可供使用者覆盖的css的属性,这样可做到自定义样式。
:host {
font-size: 20px;
background-color: var(--my-footer-bg-color, #fff);
}
/* 选择指定自定义元素的任何实例 */
my-footer:defined {
--my-footer-bg-color: #eee;
}
兼容性
HTML templates(HTML模板)
和
元素可定义可重用的HTML结构,减少使用js动态创建,也更加可视化与灵活性。
template
template模板中可以定义DOM结构,但是浏览器不会渲染,可以在custom elements中利用。
定义template内容
创建custom elements:
class myFooter extends HTMLElement {
constructor() {
// 必须先调用super方法,类的构造函数constructor总是先调用super()来建立正确的原型链继承关系。
self = super();
this._shadowRoot = this.attachShadow({ mode: 'open' })
const template = document.getElementById('my-footer-template')
// 拷贝template的内容添加到shadow dom上
this._shadowRoot.appendChild(template.content.cloneNode(true))
this.$img = this._shadowRoot.querySelector('#my-footer-img')
this.$text = this._shadowRoot.querySelector('#my-footer-text')
const img = this.getAttribute('img')
const text = this.getAttribute('text')
this.$img.setAttribute('src', img)
this.$text.textContent = text
}
}
customElements.define('my-footer', myFooter);
在定义好的template中可以清晰地看到将要添加到Shadow DOM中的文档结构,就像真正写在页面中一样。将template中的内容拷贝到Shadow DOM上,用到了Node.cloneNode()
接口。
Node.cloneNode()
Node.cloneNode()
方法返回调用该方法的节点的一个副本.
语法:const dupNode = node.cloneNode(deep);
-
node
: 将要被克隆的节点 -
dupNode
: 克隆生成的副本节点 -
deep
:可选,是否采用深度克隆,如果为true,则该节点的所有后代节点也都会被克隆;如果为false,则只克隆该节点本身
var dupNode = node.cloneNode(deep);
// 拷贝节点并不属于当前文档树的一部分,也就是说,它没有父节点,需要Node.appendChild()或其他类似的方法将拷贝的节点添加到文档中
Node.appendChild(dupNode)
slot插槽
我们在Vue中创建组件时经常会使用到
标签,该标签主要能增加插入元素的灵活性。这里也是如此,slot由其name属性标识,并允许在模板中定义占位符,当在文档中通过定义属性slot=slotName使用时,可以在占位符中填充任何HTML标记片段。
定义template及slot
This is footer-after text.
页面上渲染出来的结构为:
可以看出通过插槽的方式,可以定义占位符,后续复用自定义元素的时候,既可以有复用性的一面,又有自定义内容的一面,两者结合,完美。
兼容性
应用
看完上文,应该多多少少也了解了web组件的特性及作用,这里总结下web组件可以用来做什么:
- 构建不依赖任何框架(如Vue,React,Angular等)的可重用组件库
- 创建具有封装性的自定义组件标签,隔离HTML与CSS
- 可用于挂载一个独立功能的Shadow DOM,减少dom深层嵌套及与主文档的相互影响
web组件构建框架
目前有一些框架专门用来构建web组件,可以更加方便我们去构建组件库。
- Polymer library: (published by Google in 2013) The Polymer library provides a set of features for creating custom elements.
- Lit: (The Polymer library is in maintenance mode. For new development, we recommend Lit) Lit is a simple library for building fast, lightweight web components.
At Lit's core is a boilerplate-killing component base class that provides reactive state, scoped styles, and a declarative template system that's tiny, fast and expressive. - LitElement: LitElement is now part of the
Lit
library - Vue3.2+:
defineCustomElement
api. - ReactJS: Web Components
- AngularJS:
createCustomElement
api.
兼容
可在下面文档中去引入polyfills文件去兼容web组件
- webcomponents.js (v1 spec polyfills)
- Web Components Polyfills
最后
看完上文,大家应该对video标签为什么在不同浏览器展示的样式不同,以及为什么不同浏览器兼容性不一致这些问题都有了很好的理解。未来,可能会有更多浏览器内置的自定义元素出现,需要各自浏览器去兼容,这样我们就能用到更多通用的标签。
最近vue也发布了3.2.0版本,该版本全局api中就新增了defineCustomElement
接口,并且google也在不断维护其web组件创建框架,各种浏览器也在不断兼容web组件特性,未来我觉得web组件不会被埋没,而是会让更多人了解。
但由于我们现在用的大多框架都在virtual DOM
层面上操作DOM,而web组件是脱离框架的,这使得需要纯JS去操作DOM,这可能需要回到最初的虚拟DOM前的时代去创建,可能会带来开发上的繁琐,这也是一个值得思考与改进的问题。
更多阅读
- Web Components
- webcomponents.org
- Using custom elements
- Custom elements
- Using shadow DOM
- Shadow DOM v1: Self-Contained Web Components