在现代浏览器中,通过 class 继承 HTMLElement 可以轻松创建原生 Web Components(自定义元素),并能享受与普通 HTML 元素同等的语义和性能优势。下面将从核心概念、生命周期方法、Shadow DOM、表单关联、自定义属性、以及与 Vue 3 的集成等方面,详细说明如何使用
概览
继承 HTMLElement 起点于 ECMAScript 的 extends 关键字,它用于在类声明或表达式中创建子类。自定义元素本质上是一个继承自 HTMLElement(或某个内置元素接口)的类,注册后即可像普通标签一样使用,同时可在内部封装状态、事件和样式
JavaScript 中的 extends 与 HTMLElement 接口
- extends 关键字
extends 用于创建子类,保证子类实例具有父类的属性和方法;必须在子类构造函数中首先调用 super()
- HTMLElement 接口
HTMLElement 是所有 HTML 元素的基础接口,提供访问 DOM 属性、事件和方法,如 id、innerHTML、click() 等
- 定义自定义元素
自主式(Autonomous) vs. 内置扩展(Customized built-in),自主式元素:直接继承 HTMLElement,需从零实现行为,内置扩展元素:继承特定接口(如 HTMLParagraphElement),在保留原有行为基础上添加新功能;但 Safari 尚不支持此模式
最小示例
class MyPopup extends HTMLElement {
constructor() {
super(); // 必须调用
// 初始状态或事件绑定
}
connectedCallback() {
this.textContent = 'Hello Web Component';
}
}
customElements.define('my-popup', MyPopup);
浏览器解析到 时,会实例化该类并调用相应生命周期方法
生命周期回调
自定义元素提供多种回调,可在不同阶段执行逻辑
- connectedCallback():元素插入文档时触发,推荐在此完成 DOM 渲染和事件绑定
- disconnectedCallback():元素从文档移除时触发,用于清理资源。
- adoptedCallback():元素被移动到新文档时触发。
- attributeChangedCallback(name, oldV, newV):监听属性变化,需通过静态 observedAttributes 指定监控列表
Shadow DOM 与样式封装
-
通过 this.attachShadow({ mode: 'open' }) 创建 Shadow 根,实现样式和 DOM 的隔离
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML =<style>p { color: red; }</style><p><slot></slot></p>
;
} -
插槽 支持内容投影。
-
Shadow DOM 规范详情可参阅 MDN <<Using shadow DOM>>
表单关联(Form-associated)
class MyCheckbox extends HTMLElement {
static formAssociated = true; // 启用表单关联
constructor() {
super();
this._internals = this.attachInternals(); // 获取 ElementInternals
}
// ...实现 name、value、checked 等属性
}
customElements.define('my-checkbox', MyCheckbox);
- attachInternals() 返回 ElementInternals,用于控制表单行为和 ARIA 信息
TypeScript 中扩展 HTMLElement
declare global {
interface HTMLElementTagNameMap {
'my-element': MyElement;
}
}
class MyElement extends HTMLElement {
foo!: string; // 自定义属性
}
customElements.define('my-element', MyElement);
在 Vue 3 中定义原生 Custom Elements
import { defineCustomElement } from 'vue';
import MyVueComp from './MyVueComp.ce.vue';
const MyVueElement = defineCustomElement({
...MyVueComp,
styles: ['/* inlined CSS */']
});
customElements.define('my-vue-element', MyVueElement);
综合示例
<!DOCTYPE html>
<html>
<head>
<script type="module" src="my-element.js"></script>
</head>
<body>
<my-popup></my-popup>
<my-vue-element some-prop="value"></my-vue-element>
</body>
</html>
JS----
// my-element.js
class MyPopup extends HTMLElement {
constructor() {
super();
// mode: 'open' 允许外部脚本访问 element.shadowRoot
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `<p>自定义弹窗内容</p>`;
}
}
customElements.define('my-popup', MyPopup);
生命周期
class MyEl extends HTMLElement {
static get observedAttributes() { return ['foo']; }
constructor() {
super(); // 必须调用
// 请勿在此访问 DOM
}
connectedCallback() {
// 渲染 UI、绑定事件
}
attributeChangedCallback(name, oldVal, newVal) {
// 响应属性变化
}
disconnectedCallback() {
// 清理事件、定时器
}
}
customElements.define('my-el', MyEl);
Shadow DOM是什么?
Shadow DOM是Web Components标准的一部分,它允许开发者将一个隐藏的、独立的DOM树附加到一个元素上。这就像是在一个主文档的DOM中创建了一个"影子"DOM,这个"影子"DOM有自己的结构和样式,与主文档的DOM相互隔离
隔离性
-
样式隔离:Shadow DOM中的样式不会泄漏到主文档中,主文档中的样式也不会影响Shadow DOM内部的元素。例如,如果在主文档中有一个全局的p标签样式(如p { color: blue; }),在Shadow DOM内部的p标签不会受到这个样式的影响,除非在Shadow DOM内部显式地引用了外部样式。
-
结构隔离:Shadow DOM内部的元素结构对于外部是隐藏的。这意味着外部JavaScript代码不能直接访问Shadow DOM内部的元素,就像它们被封装在一个黑盒中一样。例如,在一个自定义元素的Shadow DOM中,有一个元素,外部脚本不能直接通过document.querySelector('input')来获取这个输入元素,因为它被封装在Shadow DOM中
class MyShadowDOMElement extends HTMLElement {
constructor() {
super();
// 创建影子DOM
const shadow = this.attachShadow({mode: 'open'});
// 创建内部的HTML元素
const div = document.createElement('div');
div.textContent = 'Content inside Shadow DOM';
// 创建样式
const style = document.createElement('style');
style.textContent = 'div { color: blue; }';
// 将样式和元素添加到影子DOM中
shadow.appendChild(style);
shadow.appendChild(div);
}
}
customElements.define('my - shadow - dom - element', MyShadowDOMElement);
template 模版、插槽
模版
<template id="card-tpl">
<style>
.card { border:1px solid #ccc; padding:1em; border-radius:4px; }
::slotted(h2) { margin-top:0; }
</style>
<div class="card">
<header><slot name="header">默认标题2</slot></header>
<section><slot></slot></section>
<footer><slot name="footer">默认页脚</slot></footer>
</div>
</template>
js----
class MyCard extends HTMLElement {
constructor() {
super();
const tpl = document.getElementById('card-tpl').content;
this.attachShadow({mode:'open'}).appendChild(tpl.cloneNode(true));
}
}
customElements.define('my-card', MyCard);
使用---
<my-card>
<h2 slot="header">卡片标题</h2>
<p>这里是卡片主体内容。</p>
<small slot="footer">© 2025</small>
</my-card>