自定义元素可让网站开发者定义新的 HTML 标记、扩展现有标记并创建可重复使用的网络组件。
借助自定义元素,Web 开发者可以创建新的 HTML 标记、优化现有的 HTML 标记,或扩展其他开发者编写的组件。API 是 Web 组件的基础。它提供了一种基于 Web 标准的方法来使用原始 JS/HTML/CSS 来创建可重复使用的组件。其结果是代码更加精简,代码更加模块化,并且可以更好地在应用中重复使用。
简介
浏览器为我们提供了构建 Web 应用的极佳工具。这种代码叫做 HTML你可能听说过此功能!它具有声明式、可移植、支持良好且易于使用等特点。HTML 虽然很强大,但其词汇和可扩展性却是有限的。HTML 现行标准一直缺乏一种能自动将 JS 行为与您的标记关联起来的方法,直到现在为止。
自定义元素能够帮助实现 HTML 现代化、补足缺失的部分以及将结构与行为捆绑在一起。如果 HTML 无法提供问题的解决方案,我们可以创建自定义元素。自定义元素在保留 HTML 优势的同时让浏览器掌握了新技巧。
定义新元素
要定义新的 HTML 元素,我们需要 JavaScript 的强大功能!
customElements
全局用于定义自定义元素并告知浏览器新标记。使用您要创建的标记名称和扩展基本 HTMLElement
的 JavaScript class
调用 customElements.define()
。
示例 - 定义移动抽屉面板 <app-drawer>
:
class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);
// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});
用法示例:
<app-drawer></app-drawer>
请务必注意,自定义元素与使用 <div>
或任何其他元素没有什么不同。您可以在网页上声明实例,在 JavaScript 中动态创建实例,还可以附加事件监听器等。请继续阅读,了解更多示例。
定义元素的 JavaScript API
自定义元素的功能是使用扩展 HTMLElement
的 ES2015 class
定义的。扩展 HTMLElement
可确保自定义元素继承整个 DOM API,并意味着您添加到类的任何属性/方法都会成为元素的 DOM 接口的一部分。从本质上讲,使用该类为您的代码创建一个公共 JavaScript API。
示例 - 定义 <app-drawer>
的 DOM 接口:
class AppDrawer extends HTMLElement {
// A getter/setter for an open property.
get open() {
return this.hasAttribute('open');
}
set open(val) {
// Reflect the value of the open property as an HTML attribute.
if (val) {
this.setAttribute('open', '');
} else {
this.removeAttribute('open');
}
this.toggleDrawer();
}
// A getter/setter for a disabled property.
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
// Reflect the value of the disabled property as an HTML attribute.
if (val) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
}
// Can define constructor arguments if you wish.
constructor() {
// If you define a constructor, always call super() first!
// This is specific to CE and required by the spec.
super();
// Setup a click listener on <app-drawer> itself.
this.addEventListener('click', e => {
// Don't toggle the drawer if it's disabled.
if (this.disabled) {
return;
}
this.toggleDrawer();
});
}
toggleDrawer() {
// ...
}
}
customElements.define('app-drawer', AppDrawer);
在此示例中,我们将创建一个具有 open
属性、disabled
属性和 toggleDrawer()
方法的抽屉式导航栏。它还以 HTML 属性的形式反映属性。
自定义元素的一个巧妙功能是,类定义中的 this
会引用 DOM 元素本身(即类的实例)。在我们的示例中,this
引用 <app-drawer>
。这就是元素如何向自身附加 click
监听器的方式 (cy)!您不限于事件监听器。整个 DOM API 都在元素代码内提供。使用 this
访问元素的属性、检查其子项 (this.children
)、查询节点 (this.querySelectorAll('.items')
) 等。
有关创建自定义元素的规则
- 自定义元素的名称必须包含短划线 (-)。因此,
<x-tags>
、<my-element>
和<my-awesome-app>
均为有效名称,而<tabs>
和<foo_bar>
则无效。这个要求是为了让 HTML 解析器能够区分自定义元素与常规元素。此外,当向 HTML 添加新标记时,这还能确保向前兼容性。 - 您不能多次注册同一代码。如果尝试这样做,系统会抛出
DOMException
。将新代码告知浏览器后,浏览器就完成了。不支持撤回。 - 自定义元素不能自闭合,因为 HTML 仅允许少数元素自闭。始终编写结束标记 (
<app-drawer></app-drawer>
)。
自定义元素回应
自定义元素可以定义特殊的生命周期钩子,以便在其存续的特定时间内运行代码。这称为自定义元素回应。
名称 | 调用时间 |
---|---|
constructor |
创建或升级元素的实例。适用于初始化状态、设置事件监听器或创建 Shadow DOM。
请参阅
规范
,了解您可以在 constructor 中执行的操作的相关限制。
|
connectedCallback |
每次元素插入到 DOM 时调用。适用于运行设置代码,例如提取资源或渲染。通常,您应该尝试将工作延迟至此时间。 |
disconnectedCallback |
每次从 DOM 中移除元素时调用。有助于运行清理代码。 |
attributeChangedCallback(attrName, oldVal, newVal) |
在添加、移除、更新或替换观察到的属性时调用。当解析器创建元素或升级元素时,系统也会调用此方法来获取初始值。注意:只有 observedAttributes 属性中列出的属性才会收到此回调。
|
adoptedCallback |
自定义元素已移至新的 document (例如,某个名为 document.adoptNode(el) 的用户)。
|
响应回调是同步的。如果有人对您的元素调用 el.setAttribute()
,浏览器会立即调用 attributeChangedCallback()
。同样,从 DOM 中移除您的元素(例如,用户调用 el.remove()
)后,您也会立即收到 disconnectedCallback()
。
示例:向 <app-drawer>
添加自定义元素回应:
class AppDrawer extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// ...
}
connectedCallback() {
// ...
}
disconnectedCallback() {
// ...
}
attributeChangedCallback(attrName, oldVal, newVal) {
// ...
}
}
必要时定义回应。如果您的元素足够复杂,并在 connectedCallback()
中打开与 IndexedDB 的连接,请在 disconnectedCallback()
中执行必要的清理工作。但要小心!您不能指望在任何情况下都能从 DOM 中移除元素。例如,如果用户关闭标签页,系统永远不会调用 disconnectedCallback()
。
属性和特性
将属性映射到属性
HTML 属性通常会以 HTML 属性的形式将其值传回 DOM。例如,当 hidden
或 id
的值在 JS 中发生更改时:
div.id = 'my-id';
div.hidden = true;
这些值会作为属性应用于活动 DOM:
<div id="my-id" hidden>
这称为“将属性映射到属性”。几乎所有 HTML 属性都会这样。原因何在?属性也有助于以声明方式配置元素,并且无障碍功能和 CSS 选择器等某些 API 依赖于属性正常工作。
如果您希望让元素的 DOM 表示法与其 JavaScript 状态保持同步,反射属性会非常有用。您可能希望反映属性的其中一个原因是,用户定义的样式会在 JS 状态发生变化时应用。
回想一下我们的 <app-drawer>
。此组件的使用者可能希望该组件在停用时使其淡出和/或阻止用户互动:
app-drawer[disabled] {
opacity: 0.5;
pointer-events: none;
}
当 disabled
属性在 JS 中发生更改时,我们希望该属性添加到 DOM 中,以便用户的选择器能够匹配。该元素可以通过将值映射到同名属性来提供该行为:
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
// Reflect the value of `disabled` as an attribute.
if (val) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
this.toggleDrawer();
}
观察属性更改
HTML 属性是用户声明初始状态的便捷方式:
<app-drawer open disabled></app-drawer>
元素可以通过定义 attributeChangedCallback
来响应属性更改。每次对 observedAttributes
数组中列出的属性进行更改时,浏览器都会调用此方法。
class AppDrawer extends HTMLElement {
// ...
static get observedAttributes() {
return ['disabled', 'open'];
}
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
if (val) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
}
// Only called for the disabled and open attributes due to observedAttributes
attributeChangedCallback(name, oldValue, newValue) {
// When the drawer is disabled, update keyboard/screen reader behavior.
if (this.disabled) {
this.setAttribute('tabindex', '-1');
this.setAttribute('aria-disabled', 'true');
} else {
this.setAttribute('tabindex', '0');
this.setAttribute('aria-disabled', 'false');
}
// TODO: also react to the open attribute changing.
}
}
在示例中,当 disabled
属性发生更改时,我们将为 <app-drawer>
设置其他属性。虽然我们在这里并未这样做,但您也可以使用 attributeChangedCallback
使 JS 属性与其属性保持同步。
元素升级
渐进式增强的 HTML
我们已经了解,自定义元素是通过调用 customElements.define()
定义的。但这并不意味着您必须一次性定义并注册自定义元素。
自定义元素可以在定义注册之前使用。
渐进式增强是自定义元素的一项功能。换言之,您可以在页面上声明多个 <app-drawer>
元素,并在很久之后再调用 customElements.define('app-drawer', ...)
。这是因为,由于存在未知标记,浏览器会以不同的方式处理潜在的自定义元素。调用 define()
并向现有元素赋予类定义的过程称为“元素升级”。
如需了解标记名称何时获得定义,您可以使用 window.customElements.whenDefined()
。它会返回一个在元素被定义时进行解析的 Promise。
customElements.whenDefined('app-drawer').then(() => {
console.log('app-drawer defined');
});
示例 - 推迟工作,直到一组子元素升级完成
<share-buttons>
<social-button type="twitter"><a href="...">Twitter</a></social-button>
<social-button type="fb"><a href="...">Facebook</a></social-button>
<social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');
let promises = [...undefinedButtons].map((socialButton) => {
return customElements.whenDefined(socialButton.localName);
});
// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
// All social-button children are ready.
});
元素定义的内容
自定义元素可以使用元素代码内的 DOM API 管理自己的内容。为此,回应会大有帮助。
示例 - 使用一些默认 HTML 创建元素:
customElements.define('x-foo-with-markup', class extends HTMLElement {
connectedCallback() {
this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
}
// ...
});
声明此标记将生成以下内容:
<x-foo-with-markup>
<b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>
// TODO:DevSite - 代码示例由于使用了内嵌事件处理脚本而被移除
创建使用 Shadow DOM 的元素
Shadow DOM 提供了一种方法,可让元素拥有和渲染与页面其余部分分开的 DOM 块并为其设置样式。您甚至可以使用一个标记将整个应用隐藏起来:
<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>
如需在自定义元素中使用 Shadow DOM,请在 constructor
内调用 this.attachShadow
:
let tmpl = document.createElement('template');
tmpl.innerHTML = `
<style>:host { ... }</style> <!-- look ma, scoped styles -->
<b>I'm in shadow dom!</b>
<slot></slot>
`;
customElements.define('x-foo-shadowdom', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// Attach a shadow root to the element.
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(tmpl.content.cloneNode(true));
}
// ...
});
用法示例:
<x-foo-shadowdom>
<p><b>User's</b> custom text</p>
</x-foo-shadowdom>
<!-- renders as -->
<x-foo-shadowdom>
#shadow-root
<b>I'm in shadow dom!</b>
<slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>
用户的自定义文字
// TODO:DevSite - 代码示例由于使用了内嵌事件处理脚本而被移除
从 <template>
创建元素
对于不熟悉这些例外情况的用户,您可以使用 <template>
元素声明 DOM 的 fragment,这些 fragment 在网页加载时会被解析、处于非活动状态,并且可在稍后运行时激活。它是网络组件系列中的另一个 API 基元。模板是声明自定义元素结构的理想占位符。
示例:使用通过 <template>
创建的 Shadow DOM 内容注册元素:
<template id="x-foo-from-template">
<style>
p { color: green; }
</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.</p>
</template>
<script>
let tmpl = document.querySelector('#x-foo-from-template');
// If your code is inside of an HTML Import you'll need to change the above line to:
// let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');
customElements.define('x-foo-from-template', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(tmpl.content.cloneNode(true));
}
// ...
});
</script>
这几行代码的作用非常突出。我们来了解这些发生的主要原因:
- 我们将在 HTML 中定义一个新元素:
<x-foo-from-template>
- 元素的 Shadow DOM 使用
<template>
创建 - 由于 Shadow DOM,元素的 DOM 局限于元素本地
- 得益于 Shadow DOM,元素的内部 CSS 作用域限定为相应元素
我位于 Shadow DOM 中。我的标记是基于 <template> 的。
// TODO:DevSite - 代码示例由于使用了内嵌事件处理脚本而被移除
设置自定义元素的样式
即使您的元素使用 Shadow DOM 定义了自己的样式,用户也可以在自己的页面中为自定义元素设置样式。这些样式称为“用户定义的样式”。
<!-- user-defined styling -->
<style>
app-drawer {
display: flex;
}
panel-item {
transition: opacity 400ms ease-in-out;
opacity: 0.3;
flex: 1;
text-align: center;
border-radius: 50%;
}
panel-item:hover {
opacity: 1.0;
background: rgb(255, 0, 255);
color: white;
}
app-panel > panel-item {
padding: 5px;
list-style: none;
margin: 0 7px;
}
</style>
<app-drawer>
<panel-item>Do</panel-item>
<panel-item>Re</panel-item>
<panel-item>Mi</panel-item>
</app-drawer>
您可能会问自己,如果元素在 Shadow DOM 中定义了样式,CSS 特异性如何起作用。在特异性方面,用户样式优先。它们始终会覆盖元素定义的样式。请参阅创建使用 Shadow DOM 的元素部分。
预先设置未注册元素的样式
在升级某个元素之前,您可以使用 :defined
伪类在 CSS 中定位该元素。这对于预先设置组件样式非常有用。例如,您可能希望隐藏未定义的组件并在定义后使其淡入,从而防止布局或其他视觉 FOUC。
示例 - 在定义 <app-drawer>
之前将其隐藏:
app-drawer:not(:defined) {
/* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
display: inline-block;
height: 100vh;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
定义 <app-drawer>
后,选择器 (app-drawer:not(:defined)
) 将不再匹配。
扩展元素
Custom Elements API 对创建新的 HTML 元素很有用,但也有助于扩展其他自定义元素,甚至是浏览器的内置 HTML。
扩展自定义元素
扩展另一个自定义元素可通过扩展其类定义来实现。
示例 - 创建扩展 <app-drawer>
的 <fancy-app-drawer>
:
class FancyDrawer extends AppDrawer {
constructor() {
super(); // always call super() first in the constructor. This also calls the extended class' constructor.
// ...
}
toggleDrawer() {
// Possibly different toggle implementation?
// Use ES2015 if you need to call the parent method.
// super.toggleDrawer()
}
anotherMethod() {
// ...
}
}
customElements.define('fancy-app-drawer', FancyDrawer);
扩展原生 HTML 元素
假设您想要创建一个精美的 <button>
。更好的选择是使用自定义元素逐步增强现有元素,而不是复制 <button>
的行为和功能。
自定义内置元素是一种自定义元素,用于扩展浏览器的某个内置 HTML 标记。扩展现有元素的主要好处是获得其所有功能(DOM 属性、方法、无障碍功能)。编写渐进式 Web 应用的最佳方法就是逐步增强现有的 HTML 元素。
若要扩展元素,您需要创建一个继承自正确 DOM 接口的类定义。例如,扩展 <button>
的自定义元素需要从 HTMLButtonElement
(而不是 HTMLElement
)继承。同样,扩展 <img>
的元素需要扩展 HTMLImageElement
。
示例 - 扩展 <button>
:
// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
constructor() {
super(); // always call super() first in the constructor.
this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
}
// Material design ripple animation.
drawRipple(x, y) {
let div = document.createElement('div');
div.classList.add('ripple');
this.appendChild(div);
div.style.top = `${y - div.clientHeight/2}px`;
div.style.left = `${x - div.clientWidth/2}px`;
div.style.backgroundColor = 'currentColor';
div.classList.add('run');
div.addEventListener('transitionend', (e) => div.remove());
}
}
customElements.define('fancy-button', FancyButton, {extends: 'button'});
请注意,扩展原生元素时,对 define()
的调用会略有不同。第三个必需参数会告知浏览器您要扩展哪个代码。必须这样做,因为许多 HTML 标记都共用同一个 DOM 接口。<section>
、<address>
和 <em>
(以及其他)均共用 HTMLElement
;<q>
和 <blockquote>
均共用 HTMLQuoteElement
;依此类推...指定 {extends: 'blockquote'}
可让浏览器知道您创建的是强化的 <blockquote>
,而不是 <q>
。如需查看 HTML DOM 接口的完整列表,请参阅 HTML 规范。
自定义内置元素的使用方可通过多种方式使用。开发者可以通过在原生代码上添加 is=""
属性来进行声明:
<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>
在 JavaScript 中创建实例:
// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);
或者使用 new
运算符:
let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;
下面是扩展 <img>
的另一个示例。
示例 - 扩展 <img>
:
customElements.define('bigger-img', class extends Image {
// Give img default size if users don't specify.
constructor(width=50, height=50) {
super(width * 10, height * 10);
}
}, {extends: 'img'});
用户声明此组件为:
<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">
或使用 JavaScript 创建实例:
const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);
其他详细信息
未知元素与未定义的自定义元素
HTML 使用起来非常宽松和灵活。例如,在一个网页上声明 <randomtagthatdoesntexist>
,浏览器会非常乐意接受它。为什么非标准代码会起作用?答案是 HTML 规范允许它。规范未定义的元素会被解析为 HTMLUnknownElement
。
自定义元素并非如此。如果潜在的自定义元素是使用有效名称(包含“-”)创建的,则会被解析为 HTMLElement
。您可以在支持自定义元素的浏览器中检查自定义元素。打开控制台:Ctrl+Shift+J(在 Mac 上,则按 Cmd+Opt+J)并粘贴以下代码行:
// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true
// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true
API 参考文档
customElements
全局变量定义了用于处理自定义元素的有用方法。
define(tagName, constructor, options)
在浏览器中定义新的自定义元素。
示例
customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});
get(tagName)
在给定有效的自定义元素标记名称的情况下,返回元素的构造函数。如果没有注册任何元素定义,则返回 undefined
。
示例
let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();
whenDefined(tagName)
返回在定义自定义元素时解析的 Promise。如果该元素已定义,则立即解析。如果标记名称不是有效的自定义元素名称,则拒绝。
示例
customElements.whenDefined('app-drawer').then(() => {
console.log('ready!');
});
历史记录和浏览器支持
如果您最近几年一直在关注网络组件,就会发现 Chrome 36 及更高版本实现的 Custom Elements API 版本使用 document.registerElement()
,而不是 customElements.define()
。它现在被认为是该标准的已弃用版本,称为 v0。customElements.define()
是热门,浏览器供应商正在开始实现。这称为自定义元素 v1。
如果您恰好对旧版 v0 规范感兴趣,请查看 html5rocks 文章。
浏览器支持
Chrome 54 (status)、Safari 10.1 (status) 和 Firefox 63 (status) 具有自定义元素 v1。Edge 已开始开发。
如需使用自定义元素检测功能,请检查是否存在 window.customElements
:
const supportsCustomElementsV1 = 'customElements' in window;
聚酯纤维
在广泛提供浏览器支持之前,可以使用适用于自定义元素 v1 的独立 polyfill。不过,我们建议您使用 webcomponents.js 加载器,以最佳方式加载 Web 组件 polyfill。该加载器使用功能检测来仅异步加载浏览器所需的必要轮询填充。
安装方式:
npm install --save @webcomponents/webcomponentsjs
用法:
<!-- Use the custom element on the page. -->
<my-element></my-element>
<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>
<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
function loadScript(src) {
return new Promise(function(resolve, reject) {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
WebComponents.waitFor(() => {
// At this point we are guaranteed that all required polyfills have
// loaded, and can use web components APIs.
// Next, load element definitions that call `customElements.define`.
// Note: returning a promise causes the custom elements
// polyfill to wait until all definitions are loaded and then upgrade
// the document in one batch, for better performance.
return loadScript('my-element.js');
});
</script>
总结
自定义元素提供了一种新工具,可让我们在浏览器中定义新的 HTML 标记并创建可重复使用的组件。只要将它们与 Shadow DOM 和 <template>
等其他新平台基元结合使用,我们就可以开始实现 Web 组件的宏图了:
- 用于创建和扩展可重复使用的组件的跨浏览器(网络标准)。
- 无需库或框架即可开始使用。Vanilla JS/HTML FTW!
- 提供熟悉的编程模型。它只是 DOM/CSS/HTML。
- 可以很好地与其他 Web 平台功能(Shadow DOM、
<template>
、CSS 自定义属性等)搭配使用。 - 与浏览器的开发者工具紧密集成。
- 利用现有的无障碍功能。