印前

自定义元素介绍 | WebKit

原文链接: webkit.org

一年多前, 我们公布了 slot-based shadow DOM的API, 这是一种轻量级机制,它通过允许在shadow tree元素上创建并行DOM来封装DOM,而渲染该元素无需修改常规DOM树.

今天,我们很高兴地宣布向WebKit增加自定义元素API。有了这个API,程序员们可以定义自己的HTML元素来创建可用组件,而不再需要依赖JS框架。

定义自定义元素

要定义自定义元素,只需简单调用customElements.define方法,再取个新的本地名称并确保是HTMLElement的子类即可。假设我们创建一个名为"custom-progress-bar"的自定义进度条,然后有人可以像下面这样定义元素:

class CustomProgressBar extends HTMLElement {
  constructor() {
      super();
      const shadowRoot = this.attachShadow({mode: 'closed'});
      shadowRoot.innerHTML = `
          <style>
              :host { display: inline-block; width: 5rem; height: 1rem; }
              .progress { display: inline-block; position: relative; border: solid 1px #000; padding: 1px; width: 100%; height: 100%; }
              .progress > .bar { background: #9cf; height: 100%; }
              .progress > .label { position: absolute; top: 0; left: 0; width: 100%;
                  text-align: center; font-size: 0.8rem; line-height: 1.1rem; }
          </style>
          <div class="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
              <div class="bar" style="width: 0px;"></div>
              <div class="label">0%</div>
          </div>
      `;
      this._progressElement = shadowRoot.querySelector('.progress');
      this._label = shadowRoot.querySelector('.label');
      this._bar = shadowRoot.querySelector('.bar');
  }

  get progress() { return this._progressElement.getAttribute('aria-valuenow'); }
  set progress(newPercentage) {
      this._progressElement.setAttribute('aria-valuenow', newPercentage);
      this._label.textContent = newPercentage + '%';
      this._bar.style.width = newPercentage + '%';
  }
};
customElements.define('custom-progress-bar', CustomProgressBar); 

我们可以在标记语言中用<custom-progress-bar></custom-progress-bar>实例化该元素,或者通过new CustomProgressBar动态实例化,还可以通过document.createElement("custom-progress-bar")创建该元素; 设置元素的progress属性值可以更新进度条,例如element.progress = 50,效果如图:

progress-bar

来看个生动的demo吧. 虽然我在上面使用了ES6语法,但同样可以像下面这样使用ES5规则里的构造函数来定义:

function CustomProgressBar() {
  const instance = Reflect.construct(HTMLElement, [], CustomProgressBar);
  ...
  return instance;
}
customElements.define('custom-progress-bar', CustomProgressBar); 

但是customElements.define方法的的第一个参数(即自定元素的名称)有几个限制条件:

  • 必须以小写字母a-z开头.

  • 不能包含大些字母A-Z.

  • 必须包含"-".

HTML规范 看关于有效的自定义元素的名称更精确的定义。

使用自定义元素回调

很多内置元素通过它们的属性来通信和接收数值,并响应数值的变化。我们可以用自定义元素结合它的响应回调函数达到同样的效果。例如,如果想通过data-progress属性为我们的自定义进度条元素设置进度,可以像下面这样做:

class CustomProgressBar extends HTMLElement {
  ...
  static get observedAttributes() { return ['value']; }
  attributeChangedCallback(name, oldValue, newValue, namespaceURI) {
      if (name === 'value') {
          const newPercentage = newValue === null ? 0 : parseInt(newValue);
          this._progressElement.setAttribute('aria-valuenow', newPercentage);
          this._label.textContent = newPercentage + '%';
          this._bar.style.width = newPercentage + '%';
      }
  }
  get progress() { return this.getAttribute('value'); }
  set progress(newValue) { this.setAttribute('value', newValue); }
} 
``<custom-progress-bar value="10"></custom-progress-bar>`` 

在这里,我们已经声明这个自定义元素观察observedAttributes函数中的value属性。当该属性发生添加、删除等变动时,浏览器引擎会调用attributeChangedCallback方法。请注意,当属性被删除时,newValue为null. 同样地,当刚添加属性时,oldValue也为null.

为了方便,自定义元素的API提供了几种回调函数。

  • connectedCallback() – 当自定义元素被插入到文档中时被调用.

  • disconnectedCallback() – 当自定义元素从文档中移除时被调用.

  • adoptedCallback(oldDocument, newDocument) – 当自定义元素从旧文档到新文档中被采用时被调用.

自定义元素反应的一个很好特性是它们几乎是同步的,不像MutationObserver(变动观察器)要在当前微任务结束后才传递记录。当我们调用如appendChildsetAttribute等方法时,浏览器引擎会立即调用所有必要的自定义元素响应,然后返回到调用点。这让自定义元素更容易模仿内置元素的语义,因为自定义元素有机会触发和响应DOM的变动,同时,我们又返回到启动DOM变动时的DOM API的调用点。

然而,它们在某种意义上并不是同步的,因为只有当所有的DOM变动完成后所有的回调才会被调用。 例如,Range的deleteContents()可以将多个自定义元素从文档中删除,但是这些自定义元素的disconnectedCallbacks要直到这些元素都清除后才会调用。

异步定义自定义元素

虽然我们强烈推荐仅在通过customElements.define定义了这些元素后再使用自定义元素,但也有少数情况,使用另一种方式可能更便利,即异步加载定义了自定义元素的脚本。自定义元素的接口提供了升级来支持这种方式。当我们在脚本中通过document.createElement 或在标记语言中实例化一个尚未定义的自定义元素, 浏览器引擎将其保留为一个普通的HTMLElement,当它最终通过customElements.define定义后将它升级为自定义元素的一个实例。

脚本可以通过等待customElements.whenDefined返回的promise和customElements.get检索构造函数,来确保自定义元素为可用。如:

customElements.whenDefined('custom-progress-bar').then(function () {
  let CustomProgressBar = customElements.get('custom-progress-bar');
  let instance = new CustomProgressBar;
  ...
}); 

当一个元素升级为自定义元素时,自定义元素的构造函数被调用,就像是同步地构造了一个新的元素,但其实是super()调用HTMLElement的构造函数返回一个正在升级的元素而不是构造一个全新的对象。因为当元素升级时,它已经被创建并被插入到文档中,所以这样一个元素已经可以具有属性和子节点。而当同步地构造一个自定义元素时,由HTMLElement构造函数返回的元素没有任何属性和子节点,它仍然未连接到文档中。

幸运地是, 当我们在写一个自定义元素时我们几乎都不用担心,因为 attributeChangedCallback在现有的观察属性下会自动被调用,而且如果升级元素已经关联到一个元素升级时的文档 connectedCallback也会被调用。

这里有些指南,指导你如何在构造函数中避免这个差异带来的痛苦。

  • 不要在构造函数中添加,删除,修改或访问任何属性 – 属性在同步构造期间甚至是不存在的.但可以使用attributeChangedCallback. 浏览器引擎将在解析HTML时为每个属性调用它.

  • 不要插入,删除,修改或访问子节点 – 再次强调, 子节点在同步构造期间也是不存在的。但可以使用子节点的connectedCallback来向上传递信息。

结语

自定义元素的API已经实现了并在 Safari 开发预览版18默认启用,我们还处于识别和修复Shadow DOM API遗留bugs的最后阶段。我们真的很兴奋,最终通过这两个功能向Web平台提供模块化的强大功能。