自定义元素介绍
一年多前,我们宣布引入基于插槽的 Shadow DOM API,这是一种轻量级机制,通过允许在元素上创建名为“影子树”的并行 DOM 树来封装 DOM 树,该影子树替换元素的渲染而无需修改常规 DOM 树。
今天,我们很高兴地宣布 WebKit 中新增了 Custom Elements API。借助此 API,开发者可以通过定义自己的 HTML 元素来创建可用的组件,而无需依赖 JS 框架。
定义自定义元素
要定义自定义元素,只需使用元素的新局部名称和 HTMLElement
的子类来调用 customElements.define
。假设我们要创建一个名为 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')
,并通过例如 element.progress = 50
来更新其进度

请查看实时演示。尽管我上面使用了 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
。
Custom Elements API 还提供其他几种便捷的回调类型:
connectedCallback()
– 当自定义元素被插入到文档中时调用。disconnectedCallback()
– 当自定义元素从文档中移除时调用。adoptedCallback(oldDocument, newDocument)
– 当自定义元素从旧文档被“收养”到新文档时调用。
自定义元素响应的一个优点是它们几乎是同步的,这与 MutationObserver
不同,后者在当前微任务结束时才交付其记录。当我们调用 appendChild
和 setAttribute
等方法时,浏览器引擎会立即调用所有必要的自定义元素响应,然后才返回到调用点。这使得自定义元素更容易模仿内置元素的语义,因为在 DOM API 调用者返回时(该调用者启动了 DOM 突变),自定义元素有机会运行并响应 DOM 突变。
然而,它们并非在所有 DOM 突变都完成后才调用所有回调的意义上是同步的。例如,Range 的 deleteContents()
可能会从文档中删除多个自定义元素,但这些自定义元素上的 disconnectedCallback
不会在所有删除操作完成后才被调用。
异步定义自定义元素
虽然我们强烈建议仅在使用 customElements.define
定义了自定义元素之后再使用它们,但在某些情况下,异步加载定义自定义元素的脚本可能会很方便。Custom Elements API 通过升级的方式支持这种场景。当我们通过 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;
...
});
当元素升级为自定义元素时,自定义元素的构造函数会被调用,就像同步构造新元素一样,但对 HTMLElement
构造函数的 super()
调用会返回正在升级的元素,而不是构造一个全新的对象。由于元素在升级时已经创建并插入到文档中,因此这样的元素可能已经拥有属性和子节点。当同步构造自定义元素时,HTMLElement
构造函数返回的元素不包含任何属性或子节点,并且它仍与文档断开连接。
幸运的是,在编写自定义元素时,我们几乎不必担心这种差异,因为当元素升级时,attributeChangedCallback
会自动在现有被观察的属性上调用,如果升级后的元素已连接到文档,则会调用 connectedCallback
。
以下是关于在构造函数内部应避免操作的一些小指南,这样我们就不必因此差异而遇到任何痛点:
- 不要在构造函数内部添加、删除、修改或访问任何属性——在同步构造期间,属性甚至不存在。请改用
attributeChangedCallback
。浏览器引擎在解析 HTML 时会为每个属性调用它。 - 不要插入、删除、修改或访问子节点——同样,在同步构造期间,子节点甚至不存在。请使用子节点的
connectedCallback
并向上级传递信息。
总结
Custom Elements API 已在 Safari 技术预览版 18 中默认实现并启用。我们还在识别和修复 Shadow DOM API 中剩余错误的最后阶段。我们非常高兴能够通过这两个功能,最终将模块化的力量带给 Web 平台。