什么是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>

欢迎关注我的个人公众号:

参考链接:

https://developer.mozilla.org...

03-05 14:06