什么是Web Components?就是类似Vue/React组件,但浏览器直接支持的组件。
它由三种主要技术组成,可以一起使用来创建具有封装功能的通用自定义元素,可以在任何地方重复使用,而无需担心代码冲突。
- Custom Elements:一组 JavaScript API,允许定义自定义元素及其行为,然后可以页面中根据需要使用它们。
- Shadow DOM:一组 JavaScript API,用于将封装的"影子"DOM 树附加到元素(与主文档 DOM 分开呈现)并控制相关功能。通过这种方式,你可以将元素的特性保密,这样它们就可以编写脚本和设置样式,而不必担心与文档的其他部分发生冲突。
- HTML templates:<template>和<slot>元素使你能够编写未在呈现页面中显示的标记模板。然后可以多次重复使用这些作为自定义元素结构的基础。
一、实现一个Web组件
实现一个Web组件,只需完成以下步骤即可:
1、创建一个类,可以在其定义自定义组件的功能。
2、使用CustomElementRegistry.define()注册自定义组件元素,向其传递要定义的元素名称、指定其功能的类或函数,以及可选的继承自哪个元素。
3、如果需要,使用 Element.attachShadow() 方法将影子 DOM 附加到自定义元素。使用常规 DOM 方法将子元素、事件侦听器等添加到 shadow DOM。
4、如果需要,使用 <template> 和 <slot> 定义 HTML 模板。再次使用常规 DOM 方法克隆模板并将其附加到您的影子 DOM。
5、在页面上的任意位置使用自定义元素,就像使用任何常规 HTML 元素一样。
二、使用自定义组件
Web 文档上自定义元素的控制器是 CustomElementRegistry 对象——这个对象允许你在页面上注册一个自定义元素,返回关于注册了哪些自定义元素的信息等。你需要了解以下3个内容:
- 注册-CustomElementRegistry.define()
- 生命周期
- 两种类型的自定义元素
(一)注册-CustomElementRegistry.define()
有3个参数:
- 一个 DOMString 表示元素名称。请注意,自定义元素名称需要在其中使用破折号,它们不能是单个词。
- 定义元素行为的类对象。
- 可选,一个包含 extends 属性的选项对象,该属性指定你的元素继承自的内置元素(如果有)(仅与自定义内置元素相关)。
例如:这个元素叫做 word-count,它的类对象是 WordCount,它扩展了 <p> 元素。
customElements.define('word-count', WordCount, { extends: 'p' });
WordCount结构如下:
class WordCount extends HTMLParagraphElement {
constructor() {
// Always call super first in constructor
super();
// Element functionality written in here
...
}
}
(二)生命周期
可以在自定义元素的类定义中定义多个不同的回调,它们在元素生命周期的不同点触发:
- connectedCallback:每次将自定义元素附加到文档连接元素时调用。每次移动节点时都会发生这种情况,并且可能在元素的内容完全解析之前发生。
注意:一旦元素不再连接,可能会调用 connectedCallback,请使用 Node.isConnected 来确保。 - disconnectedCallback:每次自定义元素与文档的 DOM 断开连接时调用。
- adoptedCallback:每次将自定义元素移动到新文档时调用。
attributeChangedCallback:每次添加、删除或更改自定义元素的属性之一时调用。在静态调用observedAttributes方法中指定要注意更改的属性。
注意:要在属性更改时触发 attributeChangedCallback() 回调,必须观察这些属性。这是通过在自定义元素类中指定一个静态的 get observeAttributes() 方法来完成的 - 这应该返回一个包含你要观察的属性名称的数组:static get observedAttributes() { return ['c', 'l']; }
(三)两种类型的自定义元素
- Autonomous custom elements
独立的——它们不继承自标准的 HTML 元素。你可以通过将它们字面上写为 HTML 元素来在页面上使用它们。例如 <popup-info> 或 document.createElement("popup-info")。 - Customized built-in elements
继承自基本的 HTML 元素。要创建其中之一,你必须指定它们扩展的元素,并且通过设置基本元素的 is 属性指定自定义元素的名称来使用它们。例如 <p is="word-count"> 或 document.createElement("p", { is: "word-count" })。
1、Autonomous custom elements
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
// write element functionality in here
...
}
}
最后,注册自定义组件:
customElements.define('popup-info', PopUpInfo);
如果你想引入外部样式文件的方式引入样式:
// Apply external styles to the shadow dom
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');
// Attach the created element to the shadow dom
shadow.appendChild(linkElem);
2、Customized built-in elements
class ExpandingList extends HTMLUListElement {
constructor() {
// Always call super first in constructor
super();
// write element functionality in here
...
}
}
最后,注册自定义组件:
customElements.define('expanding-list', ExpandingList, { extends: "ul" });
使用内置自定义组件,还有点不同:照常使用 <ul> 元素,但在 is 属性中需指定自定义元素的名称。
<ul is="expanding-list">
...
</ul>
三、Using shadow DOM
Web 组件的一个重要方面是封装——能够将标记结构、样式和行为隐藏起来并与页面上的其他代码分开,这样不同的部分就不会发生冲突,并且代码可以保持整洁。Shadow DOM API 是其中的关键部分,它提供了一种将隐藏的分离 DOM 附加到元素的方法。
Shadow DOM 允许隐藏的 DOM 树附加到常规 DOM 树中的元素——这个 shadow DOM 树从一个影子根开始,在它下面可以附加到任何你想要的元素,就像普通 DOM 一样。
- Shadow host:Shadow DOM 附加到的常规 DOM 节点。
- Shadow tree:Shadow DOM 内部的 DOM 树。
- Shadow boundary:Shadow DOM 结束的地方,普通 DOM 开始的地方。
- Shadow root:Shadow tree的根节点。
你可以以与非影子节点完全相同的方式影响影子 DOM 中的节点——例如,附加子项或设置属性、使用 element.style.foo 为单个节点设置样式,或在 <style> 内为整个影子 DOM 树添加样式元素。不同之处在于 shadow DOM 内部的任何代码都不会影响其外部的任何内容,从而可以方便地进行封装。
请注意,shadow DOM 无论如何都不是什么新鲜事物——浏览器已经使用它来封装元素的内部结构很长时间了。以一个 <video> 元素为例,它暴露了默认的浏览器控件。你在 DOM 中看到的只是 <video> 元素,但它的 shadow DOM 中包含一系列按钮和其他控件。shadow DOM 规范已经允许你实际操作你自己的自定义元素的 shadow DOM。
(一)shadow DOM 用法
可以使用 Element.attachShadow() 方法将shadow root附加到任何元素。它将包含一个option(mode)的选项对象作为其参数,其值为 open 或 closed:
let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});
open 意味着你可以使用在主页面上下文中编写的 JavaScript 访问 shadow DOM,例如使用 Element.shadowRoot 属性:
let myShadowDom = myCustomElem.shadowRoot;
当需要将 shadow DOM 附加到元素时,操作它只需使用与常规 DOM 操作相同的 DOM API:
let para = document.createElement('p');
shadow.appendChild(para);
// etc.
示例:
// HTML
<label for="cvc">Enter your CVC <popup-info img="img/alt.png" data-text="Your card validation code (CVC) is an extra security feature — it is the last 3 or 4 numbers on the back of your card."></popup-info></label>
<input type="text" id="cvc">
// JS
// Create a class for the element
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
// Create a shadow root
const shadow = this.attachShadow({mode: 'open'});
// Create spans
const wrapper = document.createElement('span');
wrapper.setAttribute('class', 'wrapper');
const icon = document.createElement('span');
icon.setAttribute('class', 'icon');
icon.setAttribute('tabindex', 0);
const info = document.createElement('span');
info.setAttribute('class', 'info');
// Take attribute content and put it inside the info span
const text = this.getAttribute('data-text');
info.textContent = text;
// Insert icon
let imgUrl;
if(this.hasAttribute('img')) {
imgUrl = this.getAttribute('img');
} else {
imgUrl = 'img/default.png';
}
const img = document.createElement('img');
img.src = imgUrl;
icon.appendChild(img);
// Apply external styles to the shadow dom
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');
// Attach the created element to the shadow dom
shadow.appendChild(linkElem);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
}
}
// Define the new element
customElements.define('popup-info', PopUpInfo);
显示结果:
(二)Using templates and slots
使用 <template> 和 <slot> 元素创建灵活的模板,然后使用该模板来填充 Web 组件的 shadow DOM。
例如:
<template id="my-paragraph">
<p>My paragraph</p>
</template>
这不会出现在你的页面中,除非你使用JavaScript获取它的内容,然后将内容附加到 DOM中:
let template = document.getElementById('my-paragraph');
let templateContent = template.content;
document.body.appendChild(templateContent);
(三)Using templates with web components
模板本身很有用,但它们与 Web 组件一起工作得更好。让我们定义一个 web 组件,它使用我们的模板作为它的 shadow DOM 的内容。我们将其称为 <my-paragraph>:
customElements.define('my-paragraph',
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById('my-paragraph');
let templateContent = template.content;
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(templateContent.cloneNode(true));
}
}
);
<template id="my-paragraph">
<style>
p {
color: white;
background-color: #666;
padding: 5px;
}
</style>
<p>My paragraph</p>
</template>
在HTML中直接使用:
<my-paragraph></my-paragraph>
这里要注意的关键点是我们将模板内容的克隆附加到shadow root,使用 Node.cloneNode() 方法创建。
因为我们将其内容附加到 shadow DOM,我们可以在模板内的 <style> 元素中包含一些样式信息,然后将其封装在自定义元素中。如果我们只是将它附加到标准 DOM,这将不起作用。
(四)Adding flexibility with slots
Slots由它们的 name 属性标识,并允许你在模板中定义占位符,当在标记中使用元素时,可以用你想要的任何标记片段填充该占位符。
因此,如果我们想在我们的简单示例中添加一个插槽,我们可以像这样更新模板的段落元素:
<p><slot name="my-text">My default text</slot></p>
如果在标记中包含元素时未定义插槽的内容,或者浏览器不支持插槽,则 <my-paragraph> 仅包含后备内容“My default text”。
为了定义槽的内容,我们在 <my-paragraph> 元素中包含了一个 HTML 结构,它的槽属性的值等于我们希望它填充的槽的名称。和以前一样,这可以是你喜欢的任何内容,例如:
<my-paragraph>
<span slot="my-text">Let's have some different text!</span>
</my-paragraph>
<my-paragraph>
<ul slot="my-text">
<li>Let's have some different text!</li>
<li>In a list!</li>
</ul>
</my-paragraph>