pin_drop当前位置:知识文库 ❯ 图文
布局结构:HTML Custom Elements - 完整教程与代码示例
一、教程简介
Custom Elements(自定义元素)是Web Components技术栈的基础,它允许开发者创建具有自定义行为和属性的HTML元素。自定义元素与浏览器内置元素享有相同的能力,可以响应属性变化、参与表单提交、支持无障碍访问等。本教程将全面讲解自定义元素的注册方式、生命周期回调、属性观察机制以及高级用法。
二、核心概念
什么是Custom Elements
Custom Elements允许开发者定义新的HTML标签,或扩展现有HTML标签的功能。这些自定义元素可以拥有自己的属性、方法、事件和样式。
自定义元素的类型
自定义元素命名规则
-
必须包含连字符(
-) -
不能以
x-、polymer-等保留前缀开头 -
全部小写字母
-
名称唯一,不能重复注册
代码示例
<!-- 合法命名 -->
<my-element></my-element>
<custom-button></custom-button>
<app-card></app-card>
<!-- 非法命名 -->
<myelement></myelement> <!-- 无连字符 -->
<My-Element></My-Element> <!-- 大写字母 -->
<x-card></x-card> <!-- 保留前缀 -->三、语法与用法
注册自定义元素
代码示例
// 方式1:独立自定义元素
class MyElement extends HTMLElement {
// ...
}
customElements.define('my-element', MyElement);
// 方式2:扩展内置元素
class MyButton extends HTMLButtonElement {
// ...
}
customElements.define('my-button', MyButton, { extends: 'button' });
// 使用扩展内置元素
// <button is="my-button">点击</button>生命周期回调
代码示例
class MyElement extends HTMLElement {
// 1. 构造函数 - 元素实例化时调用
constructor() {
super(); // 必须先调用super()
// 初始化状态、创建Shadow DOM
}
// 2. 连接回调 - 元素插入DOM时调用
connectedCallback() {
// 添加事件监听、启动定时器、发起请求
}
// 3. 断开回调 - 元素移除DOM时调用
disconnectedCallback() {
// 清理事件监听、停止定时器
}
// 4. 采用回调 - 元素移到新文档时调用
adoptedCallback() {
// 处理文档迁移(如iframe之间)
}
// 5. 属性变化回调 - 观察的属性变化时调用
attributeChangedCallback(name, oldValue, newValue) {
// 响应属性变化,更新UI
}
}属性观察
代码示例
class MyElement extends HTMLElement {
// 声明需要观察的属性列表
static get observedAttributes() {
return ['value', 'disabled', 'label'];
}
attributeChangedCallback(name, oldValue, newValue) {
// name: 变化的属性名
// oldValue: 之前的值
// newValue: 新的值
switch (name) {
case 'value':
this._value = newValue;
this._render();
break;
case 'disabled':
this._disabled = newValue !== null;
this._updateDisabled();
break;
}
}
}属性反射
代码示例
class MyElement extends HTMLElement {
// Getter/Setter实现属性反射
get value() {
return this.getAttribute('value') || '';
}
set value(val) {
this.setAttribute('value', val);
}
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
if (val) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
}
}customElements全局API
代码示例
// 定义自定义元素
customElements.define('my-element', MyElement);
// 获取已定义的元素类
const MyElementClass = customElements.get('my-element');
// 等待元素定义完成
customElements.whenDefined('my-element').then(() => {
console.log('my-element 已定义');
});
// 获取已升级的元素
// 元素从未定义状态升级为已定义状态时触发四、代码示例
示例1:Custom Elements完整演示
以下是一个完整的Custom Elements演示,包含可折叠面板、评分组件、标签输入组件和扩展内置按钮四个实用组件:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Elements完整演示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Microsoft YaHei", sans-serif;
background: #f5f5f5;
padding: 20px;
}
h1 { color: #2c3e50; margin-bottom: 30px; }
.demo-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h2 { color: #2c3e50; margin-bottom: 15px; font-size: 1.1rem; }
.desc { color: #666; font-size: 13px; margin-bottom: 15px; line-height: 1.6; }
.demo-row {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.btn {
padding: 8px 16px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.btn:hover { background: #2980b9; }
.btn-danger { background: #e74c3c; }
.btn-success { background: #27ae60; }
</style>
</head>
<body>
<h1>Custom Elements自定义元素</h1>
<!-- 可折叠面板 -->
<div class="demo-section">
<h2>1. 可折叠面板(collapsible-panel)</h2>
<collapsible-panel title="什么是Custom Elements?" open>
Custom Elements是Web Components的核心技术之一,允许开发者创建具有自定义行为的HTML元素。
</collapsible-panel>
<collapsible-panel title="如何注册自定义元素?">
使用customElements.define()方法注册自定义元素。
</collapsible-panel>
<collapsible-panel title="生命周期回调有哪些?">
主要的生命周期回调包括:constructor、connectedCallback、disconnectedCallback等。
</collapsible-panel>
</div>
<!-- 评分组件 -->
<div class="demo-section">
<h2>2. 评分组件(rating-stars)</h2>
<p class="desc">点击星星设置评分,支持属性观察和事件派发</p>
<div class="demo-row">
<rating-stars value="3" label="商品质量"></rating-stars>
<rating-stars value="4" label="服务态度" color="#e74c3c"></rating-stars>
<rating-stars value="0" label="物流速度" color="#27ae60"></rating-stars>
</div>
</div>
<!-- 标签输入组件 -->
<div class="demo-section">
<h2>3. 标签输入组件(tag-input)</h2>
<tag-input placeholder="输入标签后按回车添加"></tag-input>
</div>
<!-- 扩展内置元素 -->
<div class="demo-section">
<h2>4. 扩展内置按钮(confirm-button)</h2>
<p class="desc">点击按钮后需要二次确认才执行操作</p>
<button is="confirm-button" data-message="确定要删除这条记录吗?">删除记录</button>
</div>
<script>
// ===== 可折叠面板组件 =====
class CollapsiblePanel extends HTMLElement {
static get observedAttributes() { return ['title', 'open']; }
constructor() {
super();
this.attachShadow({ mode: 'open' });
var template = document.getElementById('collapsibleTemplate');
this.shadowRoot.appendChild(template.content.cloneNode(true));
this.shadowRoot.querySelector('.panel__header')
.addEventListener('click', () => {
this.toggleAttribute('open');
});
}
connectedCallback() { this._render(); }
attributeChangedCallback(name, oldVal, newVal) {
this._render();
}
_render() {
var title = this.getAttribute('title') || '折叠面板';
var titleEl = this.shadowRoot.querySelector('.panel__title');
if (titleEl) titleEl.textContent = title;
}
}
customElements.define('collapsible-panel', CollapsiblePanel);
// ===== 评分组件 =====
class RatingStars extends HTMLElement {
static get observedAttributes() { return ['value', 'label', 'color']; }
constructor() {
super();
this._value = 0;
this._maxStars = 5;
this.attachShadow({ mode: 'open' });
var template = document.getElementById('ratingTemplate');
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
this._value = parseInt(this.getAttribute('value')) || 0;
this._render();
this._bindEvents();
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'value') {
this._value = parseInt(newVal) || 0;
this._renderStars();
}
if (name === 'label' || name === 'color') {
this._render();
}
}
_render() {
var label = this.getAttribute('label') || '评分';
var color = this.getAttribute('color') || '#f39c12';
var labelEl = this.shadowRoot.querySelector('.rating__label');
if (labelEl) labelEl.textContent = label;
this._renderStars(color);
}
_renderStars(color) {
color = color || this.getAttribute('color') || '#f39c12';
var container = this.shadowRoot.querySelector('.rating__stars');
if (!container) return;
container.innerHTML = '';
for (var i = 1; i <= this._maxStars; i++) {
var star = document.createElement('span');
star.className = 'rating__star';
star.textContent = i <= this._value ? '\u2605' : '\u2606';
star.style.color = i <= this._value ? color : '#ddd';
star.dataset.value = i;
container.appendChild(star);
}
var valueEl = this.shadowRoot.querySelector('.rating__value');
if (valueEl) valueEl.textContent = this._value + '/' + this._maxStars;
}
_bindEvents() {
var starsContainer = this.shadowRoot.querySelector('.rating__stars');
starsContainer.addEventListener('click', (e) => {
var star = e.target.closest('.rating__star');
if (star) {
this._value = parseInt(star.dataset.value);
this.setAttribute('value', this._value);
this.dispatchEvent(new CustomEvent('rating-change', {
detail: { value: this._value },
bubbles: true,
composed: true
}));
}
});
}
}
customElements.define('rating-stars', RatingStars);
// ===== 标签输入组件 =====
class TagInput extends HTMLElement {
static get observedAttributes() { return ['placeholder']; }
constructor() {
super();
this._tags = [];
this.attachShadow({ mode: 'open' });
var template = document.getElementById('tagInputTemplate');
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
var input = this.shadowRoot.querySelector('.tag-input__field');
input.placeholder = this.getAttribute('placeholder') || '输入标签';
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && input.value.trim()) {
e.preventDefault();
this._addTag(input.value.trim());
input.value = '';
}
});
}
_addTag(text) {
if (this._tags.includes(text)) return;
this._tags.push(text);
this._renderTags();
}
_removeTag(index) {
this._tags.splice(index, 1);
this._renderTags();
}
_renderTags() {
var container = this.shadowRoot.querySelector('.tag-input');
var input = this.shadowRoot.querySelector('.tag-input__field');
container.querySelectorAll('.tag').forEach(t => t.remove());
this._tags.forEach((tag, index) => {
var tagEl = document.createElement('span');
tagEl.className = 'tag';
tagEl.innerHTML = tag + '<span class="tag__remove">×</span>';
container.insertBefore(tagEl, input);
});
}
}
customElements.define('tag-input', TagInput);
// ===== 扩展内置按钮 =====
class ConfirmButton extends HTMLButtonElement {
constructor() {
super();
this._confirming = false;
this._originalText = '';
this._timer = null;
}
connectedCallback() {
this.addEventListener('click', this._handleClick.bind(this));
}
disconnectedCallback() {
clearTimeout(this._timer);
}
_handleClick(e) {
if (!this._confirming) {
e.preventDefault();
this._confirming = true;
this._originalText = this.textContent;
this.textContent = '确认' + this._originalText + '?';
this.style.background = '#e74c3c';
this._timer = setTimeout(() => { this._reset(); }, 3000);
} else {
this._reset();
}
}
_reset() {
this._confirming = false;
this.textContent = this._originalText;
this.style.background = '';
clearTimeout(this._timer);
}
}
customElements.define('confirm-button', ConfirmButton, { extends: 'button' });
</script>
</body>
</html>五、浏览器兼容性
提示:Customized Built-in Elements(扩展内置元素)目前仅Chrome和Edge支持,Safari和Firefox均不支持。在实际项目中,建议优先使用Autonomous Custom Elements以确保跨浏览器兼容性。
六、注意事项与最佳实践
1. constructor中必须调用super()
代码示例
class MyElement extends HTMLElement {
constructor() {
super(); // 必须首先调用
// 然后才能使用this
}
}2. 避免在constructor中读取属性
代码示例
class MyElement extends HTMLElement {
constructor() {
super();
// 不推荐:属性可能还未设置
// this._value = this.getAttribute('value');
}
connectedCallback() {
// 推荐:在这里读取属性
this._value = this.getAttribute('value');
}
}3. 使用:defined防止FOUC
代码示例
/* 未定义时隐藏,避免闪烁 */
my-element:not(:defined) {
display: none;
}
/* 定义后显示 */
my-element:defined {
display: block;
}4. 属性值始终是字符串
代码示例
attributeChangedCallback(name, oldVal, newVal) {
// HTML属性值始终是字符串,需要手动转换
if (name === 'count') {
this._count = parseInt(newVal) || 0; // 转为数字
}
if (name === 'disabled') {
this._disabled = newVal !== null; // 布尔属性判断
}
}5. 布尔属性的处理
代码示例
class MyElement extends HTMLElement {
// 布尔属性:存在为true,不存在为false
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
if (val) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled'); // 注意:用removeAttribute而非setAttribute(false)
}
}
}七、代码规范示例
以下是一个符合规范的Custom Elements组件模板,涵盖了静态属性声明、构造函数、属性Getter/Setter、生命周期回调、公共方法和私有方法等完整结构:
代码示例
// Custom Elements规范模板
class MyComponent extends HTMLElement {
// ===== 1. 静态属性 =====
static get observedAttributes() {
return ['value', 'disabled', 'label'];
}
// ===== 2. 构造函数 =====
constructor() {
super();
// 初始化私有状态
this._value = '';
this._disabled = false;
// 创建Shadow DOM
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(
document.getElementById('myTemplate').content.cloneNode(true)
);
// 缓存DOM引用
this._input = this.shadowRoot.querySelector('input');
}
// ===== 3. 属性Getter/Setter =====
get value() { return this._value; }
set value(val) {
this._value = val;
this.setAttribute('value', val);
}
get disabled() { return this._disabled; }
set disabled(val) {
this._disabled = Boolean(val);
this.toggleAttribute('disabled', this._disabled);
}
// ===== 4. 生命周期回调 =====
connectedCallback() {
this._upgradeProperties();
this._render();
this._addEventListeners();
}
disconnectedCallback() {
this._removeEventListeners();
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal === newVal) return;
switch (name) {
case 'value':
this._value = newVal || '';
break;
case 'disabled':
this._disabled = newVal !== null;
break;
}
if (this.isConnected) this._render();
}
// ===== 5. 公共方法 =====
focus() {
this._input && this._input.focus();
}
reset() {
this.value = '';
}
// ===== 6. 私有方法 =====
_upgradeProperties() {
// 处理在元素注册前设置的属性
if (this.hasOwnProperty('value')) {
const value = this.value;
delete this.value;
this.value = value;
}
}
_render() {
if (this._input) {
this._input.value = this._value;
this._input.disabled = this._disabled;
}
}
_addEventListeners() {
this._input && this._input.addEventListener('input', this._onInput.bind(this));
}
_removeEventListeners() {
this._input && this._input.removeEventListener('input', this._onInput);
}
_onInput(e) {
this._value = e.target.value;
this.dispatchEvent(new CustomEvent('input', {
detail: { value: this._value },
bubbles: true,
composed: true
}));
}
}
// ===== 7. 注册元素 =====
customElements.define('my-component', MyComponent);八、常见问题与解决方案
问题1:元素未升级(undefined state)
原因:元素在DOM中但自定义元素类尚未注册。
解决方案:使用:not(:defined)伪类隐藏未定义的元素,或使用customElements.whenDefined()等待元素定义完成。
代码示例
/* 隐藏未定义的元素 */
my-element:not(:defined) {
display: none;
}代码示例
// 等待元素定义完成
customElements.whenDefined('my-element').then(() => {
console.log('元素已定义');
});问题2:属性变化回调不触发
原因:属性未在observedAttributes中声明。
解决方案:在observedAttributes静态getter中列出所有需要观察的属性。
代码示例
static get observedAttributes() {
return ['value', 'disabled']; // 必须列出所有需要观察的属性
}问题3:扩展内置元素Safari不支持
原因:Safari不支持Customized Built-in Elements。
解决方案:使用独立自定义元素代替扩展内置元素,或在内部包裹原生元素。
代码示例
// 不推荐(Safari不支持)
class MyButton extends HTMLButtonElement {}
customElements.define('my-button', MyButton, { extends: 'button' });
// 推荐(兼容方案)
class MyButton extends HTMLElement {
constructor() {
super();
// 在Shadow DOM中创建原生button
}
}
customElements.define('my-button', MyButton);问题4:constructor中报错
原因:未调用super()或在super()之前使用this。
解决方案:确保constructor中第一行是super()调用。
九、总结
Custom Elements是Web Components的基础,关键要点如下:
-
命名规则:必须包含连字符,全小写,不能使用保留前缀
-
两种类型:独立自定义元素和扩展内置元素
-
生命周期:constructor -> connectedCallback -> attributeChangedCallback -> disconnectedCallback
-
属性观察:通过observedAttributes声明,attributeChangedCallback响应
-
属性反射:使用getter/setter同步属性和DOM特性
-
:defined:使用:defined伪类防止FOUC
-
事件通信:使用CustomEvent + composed: true实现组件间通信
-
资源清理:在disconnectedCallback中清理定时器和事件监听
-
Safari兼容:避免使用Customized Built-in Elements
常见问题
Custom Elements的命名有什么要求?
自定义元素名称必须包含连字符(-),全部使用小写字母,不能以x-、polymer-等保留前缀开头,且名称必须唯一不能重复注册。例如my-element、custom-button都是合法命名,而myelement、My-Element则是非法的。
Custom Elements有哪些生命周期回调?
Custom Elements共有5个生命周期回调:constructor(构造函数,元素实例化时调用)、connectedCallback(元素插入DOM时调用)、disconnectedCallback(元素移除DOM时调用)、adoptedCallback(元素移到新文档时调用)和attributeChangedCallback(观察的属性变化时调用)。
如何让属性变化触发自定义元素的更新?
需要通过静态getter observedAttributes声明需要观察的属性列表,然后在attributeChangedCallback回调中根据属性名和新老值进行相应的UI更新。注意属性值始终是字符串类型,需要手动进行类型转换。
为什么扩展内置元素在Safari中不工作?
Safari目前不支持Customized Built-in Elements(扩展内置元素),只支持Autonomous Custom Elements(独立自定义元素)。解决方案是使用独立自定义元素代替,或者在Shadow DOM内部包裹原生元素来实现类似功能。
如何防止自定义元素加载时的页面闪烁(FOUC)?
可以使用CSS的:defined伪类来控制未定义元素的显示。通过my-element:not(:defined) { display: none; }隐藏尚未升级的元素,等元素定义完成后再显示,从而避免页面闪烁问题。
constructor中有哪些注意事项?
constructor中第一行必须调用super(),之后才能使用this。另外不推荐在constructor中读取属性值,因为此时属性可能还未设置,应该在connectedCallback中读取初始属性值。
本文涉及AI创作
内容由AI创作,请仔细甄别