Custom elements

74 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情

我们可以通过描述带有自己的方法、属性和事件等的类来创建自定义 HTML 元素。

在 custom elements (自定义标签)定义完成之后,我们可以将其和 HTML 的内建标签一同使用。

这是一件好事,因为虽然 HTML 有非常多的标签,但仍然是有穷尽的。如果我们需要像 <easy-tabs><sliding-carousel><beautiful-upload>…… 这样的标签,内建标签并不能满足我们。

我们可以把上述的标签定义为特殊的类,然后使用它们,就好像它们本来就是 HTML 的一部分一样。

Custom elements 有两种:

  1. Autonomous custom elements (自主自定义标签)  —— “全新的” 元素, 继承自 HTMLElement 抽象类.
  2. Customized built-in elements (自定义内建元素)  —— 继承内建的 HTML 元素,比如自定义 HTMLButtonElement 等。

我们将会先创建 autonomous 元素,然后再创建 customized built-in 元素。

在创建 custom elements 的时候,我们需要告诉浏览器一些细节,包括:如何展示它,以及在添加元素到页面和将其从页面移除的时候需要做什么,等等。

通过创建一个带有几个特殊方法的类,我们可以完成这件事。这非常容易实现,我们只需要添加几个方法就行了,同时这些方法都不是必须的。

下面列出了这几个方法的概述:

class MyElement extends HTMLElement {
  constructor() {
    super();
    // 元素在这里创建
  }

  connectedCallback() {
    // 在元素被添加到文档之后,浏览器会调用这个方法
    //(如果一个元素被反复添加到文档/移除文档,那么这个方法会被多次调用)
  }

  disconnectedCallback() {
    // 在元素从文档移除的时候,浏览器会调用这个方法
    // (如果一个元素被反复添加到文档/移除文档,那么这个方法会被多次调用)
  }

  static get observedAttributes() {
    return [/* 属性数组,这些属性的变化会被监视 */];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // 当上面数组中的属性发生变化的时候,这个方法会被调用
  }

  adoptedCallback() {
    // 在元素被移动到新的文档的时候,这个方法会被调用
    // (document.adoptNode 会用到, 非常少见)
  }

  // 还可以添加更多的元素方法和属性
}

在申明了上面几个方法之后,我们需要注册元素:

// 让浏览器知道我们新定义的类是为 <my-element> 服务的
customElements.define("my-element", MyElement);

现在当任何带有 <my-element> 标签的元素被创建的时候,一个 MyElement 的实例也会被创建,并且前面提到的方法也会被调用。我们同样可以使用 document.createElement('my-element') 在 JavaScript 里创建元素。

Custom element 名称必须包括一个短横线 -

Custom element 名称必须包括一个短横线 -, 比如 my-element 和 super-button 都是有效的元素名,但 myelement 并不是。

这是为了确保 custom element 和内建 HTML 元素之间不会发生命名冲突。

例子: “time-formatted”

举个例子,HTML 里面已经有 <time> 元素了,用于显示日期/时间。但是这个标签本身并不会对时间进行任何格式化处理。

让我们来创建一个可以展示适用于当前浏览器语言的时间格式的 <time-formatted> 元素:

<script>
class TimeFormatted extends HTMLElement { // (1)

  connectedCallback() {
    let date = new Date(this.getAttribute('datetime') || Date.now());

    this.innerHTML = new Intl.DateTimeFormat("default", {
      year: this.getAttribute('year') || undefined,
      month: this.getAttribute('month') || undefined,
      day: this.getAttribute('day') || undefined,
      hour: this.getAttribute('hour') || undefined,
      minute: this.getAttribute('minute') || undefined,
      second: this.getAttribute('second') || undefined,
      timeZoneName: this.getAttribute('time-zone-name') || undefined,
    }).format(date);
  }

}

customElements.define("time-formatted", TimeFormatted); // (2)
</script>

<!-- (3) -->
<time-formatted datetime="2019-12-01"
  year="numeric" month="long" day="numeric"
  hour="numeric" minute="numeric" second="numeric"
  time-zone-name="short"
></time-formatted>
  1. 这个类只有一个方法 connectedCallback() —— 在 <time-formatted> 元素被添加到页面的时候,浏览器会调用这个方法(或者当 HTML 解析器检测到它的时候),它使用了内建的时间格式化工具 Intl.DateTimeFormat,这个工具可以非常好地展示格式化之后的时间,在各浏览器中兼容性都非常好。
  2. 我们需要通过 customElements.define(tag, class) 来注册这个新元素。
  3. 接下来在任何地方我们都可以使用这个新元素了。

Custom elements 升级

如果浏览器在 customElements.define 之前的任何地方见到了 <time-formatted> 元素,并不会报错。但会把这个元素当作未知元素,就像任何非标准标签一样。

:not(:defined) CSS 选择器可以对这样「未定义」的元素加上样式。

当 customElement.define 被调用的时候,它们被「升级」了:一个新的 TimeFormatted 元素为每一个标签创建了,并且 connectedCallback 被调用。它们变成了 :defined

我们可以通过这些方法来获取更多的自定义标签的信息:

  • customElements.get(name) —— 返回指定 custom element name 的类。
  • customElements.whenDefined(name) – 返回一个 promise,将会在这个具有给定 name 的 custom element 变为已定义状态的时候 resolve(不带值)。

在 connectedCallback 中渲染,而不是 constructor 中

在上面的例子中,元素里面的内容是在 connectedCallback 中渲染(创建)的。

为什么不在 constructor 中渲染?

原因很简单:在 constructor 被调用的时候,还为时过早。虽然这个元素实例已经被创建了,但还没有插入页面。在这个阶段,浏览器还没有处理/创建元素属性:调用 getAttribute 将会得到 null。所以我们并不能在那里渲染元素。

而且,如果你仔细考虑,这样作对于性能更好 —— 推迟渲染直到真正需要的时候。

在元素被添加到文档的时候,它的 connectedCallback 方法会被调用。这个元素不仅仅是被添加为了另一个元素的子元素,同样也成为了页面的一部分。因此我们可以构建分离的 DOM,创建元素并且让它们为之后的使用准备好。它们只有在插入页面的时候才会真的被渲染。