Web Components 原生组件化

概念

Web Components 是一个浏览器原生支持的组件化方案,其支持我们创建自定义可重用的元素,使用时不需加载任何额外的模块,其实我们一直在使用这项技术,input、video 和 audio 等就是原生的 Web Components,只是如今我们自己也可以使用这项技术去创造组件。

Web Components 主要由三项技术组成:

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

创建 Custom Element

首先,定义一个新的 JavaScript 类来表示你的 Custom Element。这个类将扩展 HTMLElement 基类。

class MyComponent extends HTMLElement {
  constructor() {
    super();
    // 在这里初始化组件状态等
  }
}

注册 Custom Element

创建好类后,你需要使用 customElements.define 方法来注册您的元素。

customElements.define('my-component', MyComponent);

注册后,你就可以在 HTML 文件中使用新标签了:

<my-component></my-component>

确保你选择的标签名包含一个连字符(-),这是自定义元素命名的必要规则。

使用 Shadow DOM

Shadow DOM 允许开发者向 DOM 树中插入一个封装的 “影子” DOM 树,影子 DOM 与主文档的 DOM 分开,意味着它们的样式和脚本都是独立的。见下面的例子:

class MyComponent extends HTMLElement {
  constructor() {
    super();
    // 创建影子 DOM 根结点
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    // 每当 custom element 连接到 DOM 时,运行这里的代码
    this.shadowRoot.innerHTML = `
      <style>
        /* Shadow DOM 中的样式在这里定义,不会影响外部文档 */
        :host { display: block; }
        p { color: blue; }
      </style>
      <p>这是 Shadow DOM 中的内容</p>
    `;
  }
}

:host 伪类使你能够定义宿主元素的样式。

connectedCallback 是 Custom Element 生命周期的一部分,当 Custom Element 被附加到 DOM 时会被调用。

我们将一个参数 { mode: "open" } 传入 attachShadow()。当 mode 设置为 "open" 时,页面中的 JavaScript 可以通过影子宿主的 shadowRoot 属性访问影子 DOM 的内部。

使用 template 和 slot

<template> 标签被用来声明一个 HTML 模板,当需要重复生成 DOM 元素时,可以用来实例化新的内容。

const template = document.createElement("template");

template.innerHTML = `
  <p>这是模板中的内容,还包含了一个 slot 元素</p>
  <slot name="my-text"></slot>
`;

在 JavaScript 中,你可以访问和克隆模板内容,然后将其附加到 Shadow DOM 中。

class MyComponent extends HTMLElement {
  constructor() {
    super();
    // 深度克隆一份 template
    const content = template.content.cloneNode(true);

    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.appendChild(content);
  }
}

然后,你可以在 HTML 中填充插槽:

<my-component>
  <span slot="my-text">这是新的内容</span>
</my-component>

生命周期回调

定义在自定义元素的类定义中的特殊回调函数。

class MyCustomElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' }).innerHTML = `...`;
    
    this.addEventListener('click', (event) => {
      console.log('元素被点击了!');
    });
  }

  // 当自定义元素第一次被连接到文档 DOM 时被调用
  connectedCallback() {
    console.log('Custom element 被添加到文档。');
  }

  // 当自定义元素与文档 DOM 断开连接时被调用
  disconnectedCallback() {
    console.log('Custom element 被移除出文档。');
  }

  // 当自定义元素被移动到新文档时被调用
  adoptedCallback() {}

  // 当自定义元素的一个属性被增加、移除或更改时被调用
  attributeChangedCallback(attrName, oldVal, newVal) {
    console.log(`属性 ${attrName}${oldVal} 变成了 ${newVal}`);
  }
}

优缺点

优点:

  • 浏览器原生支持,不用加入任何依赖
  • 多种场景适用,天生组件隔离

缺点:

  • 跟主流的框架相比,书写较为复杂,需要开发者自己进行原生 dom 操作
  • 若要写成单文件组件,需要采用模板字符串的写法,没有语法高亮,代码提示等

参考

MDN - Web Component
MDN - 使用影子 DOM
第2821期 - Web Component入门

> cd ..