Web Component即web组件,允许创建可重用的定制元素(它们的功能封装在你的代码之外)。就是 组件的功能。

web Components的概念和使用

web Components 可以创建封装了指定功能的定制元素,创建完成之后你可以在任何你喜欢的地方重用。
webComponents的三大组成部分:

  • 自定义元素:定义元素及其行为
  • 影子Dom:将封装的“影子”DOM 树附加到元素并控制其关联的功能。使用这种方式保持元素的功能私有,不用担心与文档的其他部分发生冲突。
  • html 模板:<template><slot> 元素可以作为标记模板

使用自定义元素

自定义元素即由 Web 开发人员自行定义 HTML 元素,扩展浏览器中可用的元素集。

自定义元素类型

有两种类型:

  • 自定义内置元素:继承自标准的 HTML 元素:例如 HTMLImageElement 或 HTMLParagraphElement。它们的实现定义了标准元素的行为。无需从头开始实现行为。
  • 独立自定义元素:继承自 HTML 元素基类 HTMLElement。必须从头开始实现它们的行为。

实现自定义元素

自定义元素作为一个来实现。

在类的构造函数中:
可以设置初始状态和默认值,注册事件监听器,创建一个影子根(shadow root);
不应检查元素的属性或子元素,也不应添加新的属性或子元素

  • 自定义内置元素实现格式
class WordCount extends HTMLParagraphElement {
  constructor() {
    super();
  }
  // 此处编写元素功能
}
  • 独立自定义元素实现格式
class PopupInfo extends HTMLElement {
  constructor() {
    super();
  }
  // 此处编写元素功能
}

注册自定义元素

Window.customElements.define(name,constructor,options) 方法。

  • name:自定义元素名称
  • constructor:自定义元素类
  • options:仅对于自定义内置元素,这是一个包含单个属性 extends 的对象,该属性表示要扩展的内置元素。
customElements.define("word-count", WordCount, { extends: "p" });
customElements.define("popup-info", PopupInfo);

自定义元素的回调函数

自定义元素生命周期回调包括:

  • connectedCallback():每当元素添加到文档中时调用。规范建议开发人员尽可能在此回调中实现自定义元素的设定,而不是在构造函数中实现。
  • disconnectedCallback():每当元素从文档中移除时调用。
  • adoptedCallback():每当元素被移动到新文档中时调用。
  • attributeChangedCallback():在属性更改、添加、移除或替换时调用。

配置方式,和构造函数同级:

// 为这个元素创建类
class MyCustomElement extends HTMLElement {
  static observedAttributes = ["color", "size"];
  constructor() {
    // 必须首先调用 super 方法
    super();
  }
  connectedCallback() {
    console.log("自定义元素添加至页面。");
  }
}
// 注册自定义元素
customElements.define("my-custom-element", MyCustomElement);

使用自定义元素

  • 自定义内置元素的使用
    要使用自定义内置元素,请使用内置元素,但将自定义名称作为 is 属性的值:
<p is="word-count"></p>
  • 独立自定义元素的使用
    使用独立自定义元素,就像使用内置的 HTML 元素一样,使用自定义名称即可:
<popup-info>
  <!-- 元素的内容 -->
</popup-info>

添加自定义元素的属性变化

元素的属性修改之后元素也应该响应相应的变化,所以我们需要实现自定义元素的属性变化。
实现自定义元素的属性变化需要在自定义类中添加如下两个内容:

  • 添加一个名为observedAttributes的静态属性,它是一个数组,里面包含的是标签需要变更通知的所有的属性名称。
  • 添加attributeChangedCallback() 生命周期回调函数,实现属性变更的相关操作。
    回调接受三个参数:发生变化的属性的名称、属性的旧值、属性的新值.
    attributeChangedCallback() 在元素的首次声明解析时也会被调用。

eg:

// 为这个元素创建类
class MyCustomElement extends HTMLElement {
  static observedAttributes = ["size"];

  constructor() {
    super();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`属性 ${name} 已由 ${oldValue} 变更为 ${newValue}`);
  }
}

customElements.define("my-custom-element", MyCustomElement);

自定义元素示例

  • 定义独立自定义元素

定义元素js代码

// 为当这个元素创建一个类
class PopupInfo extends HTMLElement {
  constructor() {
    // 必须首先调用 super 方法
    super();
  }

  connectedCallback() {
    // 创建影子根
    const shadow = this.attachShadow({ mode: "open" });

    // 创建几个 span
    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");

    // 获取属性内容然后将其放入 info 这个 span 内
    const text = this.getAttribute("data-text");
    info.textContent = text;

    // 插入图标
    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);

    // 创建一些 CSS 应用于影子 DOM
    const style = document.createElement("style");
    console.log(style.isConnected);

    style.textContent = `
      .wrapper {
        position: relative;
      }

      .info {
        font-size: 0.8rem;
        width: 200px;
        display: inline-block;
        border: 1px solid black;
        padding: 10px;
        background: white;
        border-radius: 10px;
        opacity: 0;
        transition: 0.6s all;
        position: absolute;
        top: 20px;
        left: 10px;
        z-index: 3;
      }

      img {
        width: 1.2rem;
      }

      .icon:hover + .info, .icon:focus + .info {
        opacity: 1;
      }
    `;

    // 将创建好的元素附加到影子 DOM 上
    shadow.appendChild(style);
    console.log(style.isConnected);
    shadow.appendChild(wrapper);
    wrapper.appendChild(icon);
    wrapper.appendChild(info);
  }
}
// 注册自定义元素
customElements.define("popup-info", PopupInfo);

在html中使用

<popup-info
  img="图片icon地址"
  data-text="悬浮显示的文字内容"></popup-info>

效果:
在这里插入图片描述

  • 定义自定义内置元素
    目标:扩展内置的 <ul> 元素,以支持展开和折叠列表项。
    定义js代码:
// 为这个元素创建类
class ExpandingList extends HTMLUListElement {
  constructor() {
    // 必须首先调用 super 方法
    // super() 的返回值是对当前元素的引用
    self = super();
  }

  connectedCallback() {
    // 获取当前自定义 ul 元素的 ul 和 li 子元素
    // 包含 ul 的 li 元素可以成为容器
    const uls = Array.from(self.querySelectorAll("ul"));
    const lis = Array.from(self.querySelectorAll("li"));
    // 隐藏所有子 ul
    // 当用户点击更高级别的容器时,这些列表就会显示出来
    uls.forEach((ul) => {
      ul.style.display = "none";
    });

    // 仔细观察每个在 ul 中的 li 元素
    lis.forEach((li) => {
      // 如果这个 li 有一个 ul 作为子元素,则对其进行装饰并添加一个点击处理程序
      if (li.querySelectorAll("ul").length > 0) {
        // 添加一个属性,以便通过样式使用
        // 来显示打开或关闭的图标
        li.setAttribute("class", "closed");
        // 将 li 元素的文本包裹在一个新的 span 元素中
        // 这样我们就可以将样式和事件处理程序分配给 span
        const childText = li.childNodes[0];
        const newSpan = document.createElement("span");

        // 从 li 复制文本到 span,设置光标样式
        newSpan.textContent = childText.textContent;
        newSpan.style.cursor = "pointer";

        // 为这个 span 添加事件处理程序
        newSpan.addEventListener("click", (e) => {
          // span 的下一个兄弟元素应该是 ul
          const nextul = e.target.nextElementSibling;

          // 切换可见状态并更新 ul 的 class 属性
          if (nextul.style.display == "block") {
            nextul.style.display = "none";
            nextul.parentNode.setAttribute("class", "closed");
          } else {
            nextul.style.display = "block";
            nextul.parentNode.setAttribute("class", "open");
          }
        });
        // 添加 span 并从 li 中移除纯文本节点
        childText.parentNode.insertBefore(newSpan, childText);
        childText.parentNode.removeChild(childText);
      }
    });
  }
}
//注册元素
customElements.define("expanding-list", ExpandingList, { extends: "ul" });

html使用:

<ul is="expanding-list">
      <li>
        列表1
        <ul is="expanding-list">
          <li>
            列表1的第一级子列表1-1
            <ul is="expanding-list">
              <li>
                列表1的第二级子列表1-1-1
                <ul is="expanding-list">
                  <li>列表1的第三级子列表1-1-1-1</li>
                  <li>列表1的第三级子列表1-1-1-2</li>
                </ul>
              </li>
              <li>列表1的第二级子列表1-1-2</li>
            </ul>
          </li>
          <li>
            列表1的第一级子列表1-2
            <ul is="expanding-list">
              <li>列表1的第二级子列表1-2-1</li>
              <li>列表1的第二级子列表1-2-2</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>列表2</li>
    </ul>

效果:
在这里插入图片描述

使用影子Dom

影子DOM的作用:保护我们的自定义元素,防止因为在页面的js代码中修改自定义元素的实现而意外地破坏我们的自定义元素。
不设置影子Dom的时候我们自定义的元素就是赤裸裸的放在页面的Dom节点中,谁想修改就可以直接获取元素进行修改,这样很可能破坏我们的自定义元素。而使用影子Dom就可以将我们的自定义元素在页面的Dom中”隐藏“,从而达到保护的效果

影子Dom的原理示意图

影子 DOM 允许将隐藏的 DOM 树附加到常规 DOM 树中的元素上——这个影子 DOM 始于一个影子根,在其之下你可以用与普通 DOM 相同的方式附加任何元素。

在这里插入图片描述
其中隐藏的dom树是不能通过document.querySelectorAll获取的,需要使用shadowRoot.querySelectorAll获取,同时我们可以设置开关来控制是否能够通过shadowRoot.querySelectorAll获取到元素(mode参数的设置),从而外界无法随意获取改变元素的效果。
如果我们将自定义的元素放置在隐藏的dom树中就可以实现保护的效果。我们需要通过影子Dom将自定义的元素放置在隐藏的dom树。

几个概念

  • 影子宿主(Shadow host): 影子 DOM 附加到的常规 DOM 节点。
  • 影子树(Shadow tree): 影子 DOM 内部的 DOM 树。
  • 影子边界(Shadow boundary): 影子 DOM 终止,常规 DOM 开始的地方。
  • 影子根(Shadow root): 影子树的根节点。

创建一个影子Dom

我们先不讨论自定义元素的情况,先理解普通元素在隐藏dom树的情况。
创建影子Dom的语法:影子宿主.attachShadow({ mode: "open" })

mode参数的作用:
当 mode 设置为 "open" 时,页面中的 JavaScript 可以通过影子宿主的 shadowRoot 属性访问影子 DOM 的内部。
当 mode 设置为 "closed" 时,页面中的 JavaScript 不能访问影子 DOM 的内部。

  • 下例中将id为host的元素设置为影子宿主创建影子Dom:
<body>
	<div id="host"></div>
	<span>I'm not in the shadow DOM</span>
</body>
<script>
	const host = document.querySelector("#host");
	const shadow = host.attachShadow({ mode: "open" });
	const span = document.createElement("span");
	span.textContent = "I'm in the shadow DOM";
	//添加为影子dom节点
	shadow.appendChild(span);
</script>

在这里插入图片描述

js隔离

影子Dom和标准Dom使用js获取节点的方式是不一样的,即js获取隔离。

  • 使用document获取元素是不会获取到影子dom上的节点的。
    添加如下js代码
 const spans = Array.from(document.querySelectorAll("span"));
  for (const span of spans) {
    span.textContent = span.textContent.toUpperCase();
  }

效果是只有标准Dom中的span元素变成大写:
在这里插入图片描述

  • 使用shadowRoot可以获取到影子Dom上的节点
    语法:影子宿主.shadowRoot.获取元素节点的方法()
    添加如下js代码
const spans = Array.from(host.shadowRoot.querySelectorAll("span"));
for (const span of spans) {
   span.textContent = span.textContent.toUpperCase();
 }

效果是只有影子Dom中的span元素变成大写:
在这里插入图片描述

  • 阻止js获取影子Dom上的节点
    创建影子Dom的时候只需要设置{mode: "closed"}就可以,此时shadowRoot返回null。

css隔离

页面中的style样式对影子Dom中的节点是不起作用的。

  • 页面中设置如下样式:
<style>
    span {
      color: rgb(173, 77, 77);
      border: 1px solid black;
      border-radius: 10px;
      background: black;
      padding: 2px 5px;
    }
  </style>

效果:
在这里插入图片描述

  • 可以使用编程式或声明式的方法为影子Dom添加样式

    • 编程式:通过构建一个 CSSStyleSheet 对象并将其附加到影子根。
      创建一个空的 CSSStyleSheet 对象
      使用 CSSStyleSheet.replace()CSSStyleSheet.replaceSync()设置其内容
      通过将其赋给 ShadowRoot.adoptedStyleSheets来添加到影子根
    const sheet = new CSSStyleSheet();
    sheet.replaceSync("span { color: red; border: 2px dotted black;}");
    shadow.adoptedStyleSheets = [sheet];
    
    • 声明式
      将一个影子Dom的 <style> 样式包含在 <template> 元素中添加到页面上。
      然后将该 <template> 元素添加到影子Dom上。
    <body>
    	<template id="my-element">
    	  <style>
    	    span {
    	      color: red;
    	      border: 2px dotted black;
    	    }
    	  </style>
    	</template>
    </body>
    <script>
    	const template = document.getElementById("my-element");
        shadow.appendChild(template.content);
    </script>
    

效果:
在这里插入图片描述

就像页面样式就像不会影响影子 DOM 中的元素一样,影子 DOM 样式也不会影响页面中其它元素的样式。

影子Dom和自定义元素

自定义元素结合使用影子Dom的优点:

  • 保护自定义元素不被破坏
  • 为自定义元素提供自己的样式空间

通常自定义元素本身是一个影子宿主,该元素在其根节点下创建多个元素以供元素的内部实现。

案例:创建一个绘制圆形的元素

<body>
  <filled-circle color="#d46b6b"></filled-circle>
</body>
<script>
  class FilledCircle extends HTMLElement {
    constructor() {
      super();
    }
    connectedCallback() {
      // 创建一个影子根
      // 自定义元素自身是影子宿主
      const shadow = this.attachShadow({ mode: "open" });

      // 创建内部实现
      const svg = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "svg"
      );
      const circle = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "circle"
      );
      circle.setAttribute("cx", "50");
      circle.setAttribute("cy", "50");
      circle.setAttribute("r", "50");
      circle.setAttribute("fill", this.getAttribute("color"));
      svg.appendChild(circle);

      shadow.appendChild(svg);
    }
  }
  customElements.define("filled-circle", FilledCircle);
</script>

在这里插入图片描述

补充: 影子根ShadowRoot的相关属性和方法

ShadowRoot的携带很多属性和方法,可以参见https://developer.mozilla.org/zh-CN/docs/Web/API/ShadowRoot

  • host: ShadowRoot.host,只读属性,返回对 ShadowRoot 所附加到的 DOM 元素的引用
  • mode: ShadowRoot.mode,只读属性,返回其模式打开或关闭。
  • activeElement : Shadow.activeElement 只读属性,返回影子树中具有焦点的元素

等。

使用html模板

之前我们定义自定义元素的时候都是在js中编写对应的元素标签,这样写无疑是比较麻烦的,我们希望可以直接在html模板中编写对应的标签和样式,然后自定义组件直接用html模板中的标签定义自己的元素。
常见的使用方法有 <template><slot> 元素

template标签实现自定义元素

利用template标签不在页面中展示的效果实现自定义元素。

<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);

因为template标签中的内容不会展示,自定义元素的时候就可以直接获取template标签中的内容使用,使用js将template标签的内容作为元素展示,并且不会影响其他内容的展示。

<body>
	<template id="my-paragraph">
	  <style>
	    p {
	      color: white;
	      background-color: #666;
	      padding: 5px;
	    }
	  </style>
	  <p>My paragraph</p>
	</template>
	<my-paragraph></my-paragraph>
</body>
<script>
	customElements.define(
	  "my-paragraph",
	  class extends HTMLElement {
	    constructor() {
	      super();
	      //   编写元素内容,这里采取将元素内容编写在html总然后直接获取插入的方法
	      let template = document.getElementById("my-paragraph");
	      let templateContent = template.content;
	
	      // 创建一个影子根
	      const shadowRoot = this.attachShadow({ mode: "open" });
	      // 使用 Node.cloneNode() 方法添加了模板的拷贝到阴影的根结点上
	      shadowRoot.appendChild(templateContent.cloneNode(true));
	    }
	  },
	);
</script>

关键点是使用 Node.cloneNode() 方法添加了模板的拷贝到阴影的根结点上。
效果:
在这里插入图片描述

slots插槽实现自定义元素

slots插槽由其name属性标识,并且允许你在模板中定义占位符,当在标记中使用该元素时,该占位符可以填充所需的任何 HTML 标记片段。

  • 占位:
<p><slot name="my-text">My default text</slot></p>
  • 在标记中使用该元素
<my-paragraph>
  <span slot="my-text">Let's have some different text!</span>
</my-paragraph>

Let's have some different text!会替换My default text显示。

  • 自定义标签使用slot插槽:
    定义标签的时候slots插槽也需要依赖template标签实现,在template标签中使用slot插槽:
<body>
    <template id="my-paragraph">
      <style>
        p {
          color: white;
          background-color: #666;
          padding: 5px;
        }
      </style>
      <p><slot name="my-text">My default text</slot></p>
    </template>

    <my-paragraph> </my-paragraph>

    <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>
  </body>
  <script>
    customElements.define(
      "my-paragraph",
      class extends HTMLElement {
        constructor() {
          super();
          //   编写元素内容,这里采取将元素内容编写在html总然后直接获取插入的方法
          let template = document.getElementById("my-paragraph");
          let templateContent = template.content;

          // 创建一个影子根
          const shadowRoot = this.attachShadow({ mode: "open" });
          // 添加自定义元素到影子根
          shadowRoot.appendChild(templateContent.cloneNode(true));
        }
      }
    );
  </script>

效果:
在这里插入图片描述

补充:插槽slot身上的属性和方法

详细参见: https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement
属性:

  • name:插槽的名字

方法:

  • assign():将插槽的手动分配节点设置为一组有序的插槽表。
  • assignedElements():返回分配给该槽(而不是其他节点)的元素序列。
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐