pin_drop当前位置:知识文库 ❯ 图文

布局结构:HTML Custom Elements - 完整教程与代码示例

一、教程简介

Custom Elements(自定义元素)是Web Components技术栈的基础,它允许开发者创建具有自定义行为和属性的HTML元素。自定义元素与浏览器内置元素享有相同的能力,可以响应属性变化、参与表单提交、支持无障碍访问等。本教程将全面讲解自定义元素的注册方式、生命周期回调、属性观察机制以及高级用法。


二、核心概念

什么是Custom Elements

Custom Elements允许开发者定义新的HTML标签,或扩展现有HTML标签的功能。这些自定义元素可以拥有自己的属性、方法、事件和样式。

自定义元素的类型

类型 说明 示例
Autonomous Custom Elements 独立的自定义元素,继承HTMLElement <my-element>
Customized Built-in Elements 扩展内置元素,继承特定HTML元素 <button is="my-button">

自定义元素命名规则

  • 必须包含连字符-

  • 不能以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">&times;</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>

五、浏览器兼容性

特性 Chrome Firefox Safari Edge IE
Custom Elements v1 63+ 63+ 10.1+ 79+ 不支持
Customized Built-in 63+ 不支持 不支持 79+ 不支持
observedAttributes 63+ 63+ 10.1+ 79+ 不支持
customElements.whenDefined 63+ 63+ 10.1+ 79+ 不支持
:defined伪类 63+ 63+ 10.1+ 79+ 不支持

提示: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中读取初始属性值。

标签: Custom Elements Web Components 自定义元素 生命周期回调 Shadow DOM 属性观察 前端组件化

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 布局结构:HTML Shadow DOM - 完整教程与代码示例 下一篇: 元信息:HTML meta标签 - 完整教程与代码示例

poll相关推荐