印前

自定义元素介绍 | WebKit

印前 · 2016-12-11翻译 · 1143阅读 原文链接 十年踪迹审校

一年多前, 我们公布了 slot-based shadow DOM API 的介绍, 这是一种轻量级的封装 DOM 树的机制,它允许在一个叫做 “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);

这样我们可以用 HTML 标签<custom-progress-bar></custom-progress-bar>实例化该元素,或者通过new CustomProgressBar动态创建,还可以通过document.createElement("custom-progress-bar")创建该元素; 然后通过设置 element.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,后者要在当前 microtask 结束后才将属性值的改变进行记录。当我们调用如appendChildsetAttribute等方法时,浏览器引擎会立即调用所有必要的自定义元素回调函数,然后返回并继续执行。这让自定义元素更容易模仿内置元素的语义,因为这种机制让我们有机会在执行并响应 DOM 改变后,立即返回引发改变的 DOM API 调用处继续往下执行(其行为就像原生元素一样——译者注)。

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

用异步方式来定义自定义元素

虽然我们强烈建议先通过customElements.define定义自定义元素,然后再使用它们,但也有少数情况,使用另一种方式可能更便利,即异步加载定义了自定义元素的脚本。自定义元素的 API 提供了一种叫做 upgrades 的方式来支持这种用法。当我们在脚本中通过document.createElement 或在文档中实例化一个尚未定义的自定义元素时,浏览器引擎将其视为普通的HTMLElement,当它最终通过customElements.define定义后,浏览器再将它 upgrades(升级)为一个自定义元素实例。

脚本可以通过等待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 中遗留 bug 的最后阶段。我们为最终通过这两个功能向 Web 平台提供了模块化的强大功能而感到欢欣鼓舞。

相关文章