Web Component 简介

是什么

Web Component是一套Web原生的技术,可以用于创建定制元素,并在Web中使用

主要构成

Web Component主要一下几个概念构成

  • Custom element(自定义元素):一组 JavaScript API,允许你定义 custom elements 及其行为,然后可以在你的用户界面中按照需要使用它们。
  • Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的“影子”DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,你可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML template(HTML 模板): templateslot元素使你可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

通过上述技术用来封装定制元素,且不用担心代码环境冲突。

使用

自定义元素

我们可以使用ES6的定义类的方法来定义自定义元素

自定义元素有两种:独立自定义元素和自定义内置元素。定义区别上主要是独立自定义元素是扩展HTMLElement类型定义的元素,而自定义内置元素则是扩展浏览器已有的元素类(例如:HTMLParagraphElement, HTMLDivElement, HTMLSpanElement)

定义

自定义内置元素最小实现:

// 定制了p元素
class WordCount extends HTMLParagraphElement {
  constructor() {
    super();
  }
  // 此处编写元素功能
}

独立自定义元素最小实现:

class PopupInfo extends HTMLElement {
  constructor() {
    super();
  }
  // 此处编写元素功能
}

注册

// 第一个参数即元素名称,注意要小写开头
customElements.define("word-count", WordCount);
customElements.define("popup-info", PopupInfo);

使用

二者在使用上有所区别

<!--自定义内置元素-->
<p is="word-count"></p>

<!--独立自定义元素-->
<popup-info></popup-info>

自定义元素生命周期

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

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

示例:

// 为这个元素创建类
class MyCustomElement extends HTMLElement {
  // 需要实现声明监听变化的属性
  static observedAttributes = ["color", "size"];

  constructor() {
    // 必须首先调用 super 方法
    super();
  }

  connectedCallback() {
    console.log("自定义元素添加至页面。");
  }

  disconnectedCallback() {
    console.log("自定义元素从页面中移除。");
  }

  adoptedCallback() {
    console.log("自定义元素移动至新页面。");
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`属性 ${name} 已变更。oldValue:${oldValue}, newValue: ${newValue}`); // 初始默认值为null
  }
}

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

影子DOM

影子 DOM(Shadow DOM)允许你将一个 DOM 树附加到一个元素上,并且使该树的内部对于在页面中运行的 JavaScript 和 CSS 是隐藏的。

image-20250708152308792

术语

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

创建示例

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="host"></div>
  <span>I'm not in the shadow DOM</span>

  <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";
    shadow.appendChild(span);

  </script>
</body>

</html>

image-20250708152914950

尝试访问

上一个示例演示了如何向常规的DOM节点插入影子节点,接下来我们尝试通过选择器方法来选择节点并修改内容。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="host"></div>
  <span>I'm not in the shadow DOM</span>
  <br />

  <button id="upper" type="button">将 span 元素转换为大写</button>
  <button id="reload" type="button">重新加载</button>


  <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";
    shadow.appendChild(span);

    const upper = document.querySelector("button#upper");
    upper.addEventListener("click", () => {
      const spans = Array.from(document.querySelectorAll("span"));
      for (const span of spans) {
        span.textContent = span.textContent.toUpperCase();
      }
    });

    const reload = document.querySelector("#reload");
    reload.addEventListener("click", () => document.location.reload());


  </script>
</body>

</html>

image-20250708153137398

点击第一个按钮可以看到我们只改变了其中一个span,而影子DOM的span并没有被改变。因为影子DOM对于js通常是隐藏的。如果要获取影子DOM节点可以通过影子宿主来获取host.shadowRoot.querySelectorAll。

[!NOTE]

注意我们插入影子节点时设置的mode
host.shadowRoot.querySelectorAll会获取到modeopen的节点,如果将mode设置为closed 则不会被host.shadowRoot.querySelectorAll获取到。另外这个不是绝对防止被获取影子节点的方式,只是提示使用者这个影子节点并未被开放。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="host"></div>
  <span>I'm not in the shadow DOM</span>
  <br />

  <button id="upper" type="button">将 span 元素转换为大写</button>
  <button id="reload" type="button">重新加载</button>


  <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";
    shadow.appendChild(span);

    const upper = document.querySelector("button#upper");
    upper.addEventListener("click", () => {
      const spans = Array.from(host.shadowRoot.querySelectorAll("span"));
      for (const span of spans) {
        span.textContent = span.textContent.toUpperCase();
      }
    });
    const reload = document.querySelector("#reload");
    reload.addEventListener("click", () => document.location.reload());
  </script>
</body>

</html>

image-20250708153402590

影子DOM样式

外部的CSS样式不会影响到影子DOM的样式,影子 DOM 样式也不会影响页面中其它元素的样式。

有两种方式设置影子DOM样式

编程式:

const sheet = new CSSStyleSheet();
sheet.replaceSync("span { color: red; border: 2px dotted black;}");

const host = document.querySelector("#host");

const shadow = host.attachShadow({ mode: "open" });
shadow.adoptedStyleSheets = [sheet];

const span = document.createElement("span");
span.textContent = "I'm in the shadow DOM";
shadow.appendChild(span);

template里定义style标签:

<template id="my-element">
  <style>
    span {
      color: red;
      border: 2px dotted black;
    }
  </style>
  <span>I'm in the shadow DOM</span>
</template>

<div id="host"></div>
<span>I'm not in the shadow DOM</span>
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const template = document.getElementById("my-element");
shadow.appendChild(template.content);

模版和插槽

使用过Vue框架对于这个就很熟悉了。来看看原生支持的方式如何书写吧

模版

使用template定义一个模版

<template>
  <p>
    这是一个模版
  </p>
</template>

除非你使用 JavaScript 获取对它的引用,然后使用类似下面的代码将其附加到 DOM 中,否则它不会出现在你的页面中:

let template = document.getElementById("my-paragraph");
let templateContent = template.content;
document.body.appendChild(templateContent);

自定义组件中使用

我们需要给模版定义一个id,用于在定义组件的时候获取模版

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./index.js"></script>
</head>

<body>
  <template id="my-paragraph">
    <style>
      p {
        color: red;
        background-color: #666;
        padding: 5px;
      }
    </style>
    <p>我的段落</p>
  </template>

  <my-paragraph></my-paragraph>

</body>

</html>
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" });
      shadowRoot.appendChild(templateContent.cloneNode(true));
    }
  },
);

[!NOTE]

我们将模版内容的克隆添加到通过 Node.cloneNode() 方法创建的影子根上

插槽

使用方式和vue模版里的很类似的

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./index.js"></script>
</head>

<body>
  <template id="my-paragraph">
    <style>
      p {
        color: red;
        background-color: #666;
        padding: 5px;
      }
    </style>
    <p>
      <slot>默认文本</slot>
    </p>
    <slot name="aaa"></slot>
  </template>

  <my-paragraph>
    <span>hehh</span>
    <span slot="aaa">123</span>
  </my-paragraph>

</body>

</html>
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" });
      shadowRoot.appendChild(templateContent.cloneNode(true));
    }
  }
);

[!NOTE]

可以被插入到槽中的节点被称为 Slotable;已经插入到槽中的节点被称为 slotted

未命名的 slot 元素将填充自定义元素中所有不含 slot 属性的顶级子节点,也包括文本节点。

Last modification:July 8, 2025
如果觉得我的文章对你有用,请随意赞赏