一、引言:为什么需要Web Components
前端框架的迭代速度令人眼花缭乱:从jQuery到Angular,再到React和Vue,每个新项目都可能选择不同的技术栈。然而,UI组件的可移植性始终是一个难题——为一个框架编写的按钮组件无法直接在另一个框架中使用。Web Components正是为了解决这一问题而诞生的浏览器原生标准,它允许开发者创建封装良好、可跨框架复用的自定义HTML元素。
Web Components由三项核心技术构成:Custom Elements用于定义自定义HTML标签;Shadow DOM提供样式与DOM的完全隔离;HTML Templates和Slots实现灵活的内容分发。本文将带你从零开始,构建两个完整的实战组件,深入掌握这项原生技术。
二、核心技术一:Custom Elements 自定义元素
Custom Elements允许开发者定义全新的HTML标签,并为其附加行为和生命周期。通过customElements.define()方法注册后,即可像使用<div>一样使用自定义标签。
2.1 创建一个基础自定义元素
// 定义类必须继承自 HTMLElement
class MyButton extends HTMLElement {
constructor() {
super();
// 元素被创建时调用
this.textContent = '点击我';
}
connectedCallback() {
// 元素被添加到DOM时调用
this.addEventListener('click', () => {
alert('按钮被点击了!');
});
}
disconnectedCallback() {
// 元素从DOM中移除时调用
}
adoptedCallback() {
// 元素被移动到新文档时调用
}
static get observedAttributes() {
// 监听属性变化
return ['disabled', 'type'];
}
attributeChangedCallback(name, oldValue, newValue) {
// 监听的属性发生变化时调用
if (name === 'disabled') {
this.style.opacity = newValue !== null ? '0.5' : '1';
}
}
}
// 注册自定义元素,名称必须包含连字符
customElements.define('my-button', MyButton);
在HTML中直接使用:<my-button type="primary"></my-button>,就像使用原生标签一样自然。生命周期回调让组件能够响应DOM变化和属性更新,完全融入浏览器的工作流程。
三、核心技术二:Shadow DOM 样式与结构隔离
没有Shadow DOM,自定义元素内的样式会与页面全局样式相互污染。Shadow DOM创建一个独立的DOM树,其中的CSS样式完全封闭,外部样式无法穿透,内部样式也不会泄漏到外部。
3.1 启用Shadow DOM
class UserCard extends HTMLElement {
constructor() {
super();
// 创建影子根,mode: 'open' 允许外部访问
const shadow = this.attachShadow({ mode: 'open' });
// 在影子DOM中构建内容
const wrapper = document.createElement('div');
wrapper.setAttribute('class', 'card');
// 内部样式完全隔离
const style = document.createElement('style');
style.textContent = `
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
font-family: system-ui;
background: white;
}
.name {
font-size: 18px;
font-weight: bold;
color: #333;
}
.email {
color: #666;
font-size: 14px;
}
/* 这个样式不会影响外部页面 */
button {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
`;
// 使用slot接收外部传入的内容
wrapper.innerHTML = `
默认用户名
默认邮箱
`;
shadow.appendChild(style);
shadow.appendChild(wrapper);
}
}
customElements.define('user-card', UserCard);
使用该组件时,外部页面可以传入自定义内容:
<user-card>
<span slot="username">张三</span>
<span slot="email">zhangsan@example.com</span>
</user-card>
即使外部页面也定义了button样式,Shadow DOM内的按钮样式也不会受到影响,反之亦然。这种隔离性使得Web Components成为构建设计系统的理想选择。
四、核心技术三:HTML Templates 与 Slots
<template>标签用于存放不会在页面加载时渲染的HTML片段,配合<slot>实现灵活的内容分发。模板可以被克隆并注入到Shadow DOM中,实现组件结构的复用。
4.1 使用template定义组件结构
<template id="rating-card-template">
<div class="rating-card">
<div class="header">
<slot name="title">默认标题</slot>
</div>
<div class="stars">
<span class="star" data-value="1">☆</span>
<span class="star" data-value="2">☆</span>
<span class="star" data-value="3">☆</span>
<span class="star" data-value="4">☆</span>
<span class="star" data-value="5">☆</span>
</div>
<div class="footer">
<slot name="comment">暂无评价</slot>
</div>
</div>
</template>
<script>
class RatingCard extends HTMLElement {
constructor() {
super();
const template = document.getElementById('rating-card-template');
const templateContent = template.content.cloneNode(true);
const shadow = this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
.rating-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
max-width: 300px;
}
.star {
font-size: 24px;
cursor: pointer;
color: #ccc;
}
.star.active {
color: #f5a623;
}
.header { font-weight: bold; margin-bottom: 8px; }
.footer { margin-top: 8px; color: #666; font-size: 14px; }
`;
shadow.appendChild(style);
shadow.appendChild(templateContent);
this._value = 0;
this._initStars(shadow);
}
_initStars(shadow) {
const stars = shadow.querySelectorAll('.star');
stars.forEach(star => {
star.addEventListener('click', () => {
const value = parseInt(star.dataset.value);
this.setRating(value, shadow);
// 派发自定义事件
this.dispatchEvent(new CustomEvent('rate-change', {
detail: { value },
bubbles: true,
composed: true
}));
});
});
}
setRating(value, shadow) {
this._value = value;
const stars = shadow.querySelectorAll('.star');
stars.forEach(s => {
if (parseInt(s.dataset.value) <= value) {
s.classList.add('active');
s.textContent = '★';
} else {
s.classList.remove('active');
s.textContent = '☆';
}
});
}
static get observedAttributes() {
return ['initial-rating'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'initial-rating' && this.shadowRoot) {
this.setRating(parseInt(newValue), this.shadowRoot);
}
}
}
customElements.define('rating-card', RatingCard);
</script>
在HTML中使用:
<rating-card initial-rating="3">
<span slot="title">产品评分</span>
<span slot="comment">质量很好</span>
</rating-card>
这个评分卡片组件通过template克隆结构,利用slot接收外部标题和评论内容,并派发CustomEvent与外部通信。它可以在React、Vue或纯HTML项目中使用,无需任何修改。
五、实战案例二:折叠面板组件
接下来构建一个更复杂的折叠面板(Accordion)组件,展示多个Web Component之间的协作和状态管理。
<template id="accordion-item-template">
<div class="accordion-item">
<div class="accordion-header">
<slot name="header">折叠标题</slot>
<span class="arrow">▼</span>
</div>
<div class="accordion-content">
<slot>折叠内容</slot>
</div>
</div>
</template>
<script>
class AccordionItem extends HTMLElement {
constructor() {
super();
const template = document.getElementById('accordion-item-template');
const content = template.content.cloneNode(true);
const shadow = this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
.accordion-item {
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 4px;
overflow: hidden;
}
.accordion-header {
padding: 12px 16px;
background: #f5f5f5;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.accordion-header:hover {
background: #ebebeb;
}
.accordion-content {
padding: 0 16px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.accordion-item.open .accordion-content {
max-height: 500px;
padding: 16px;
}
.arrow {
transition: transform 0.3s;
}
.accordion-item.open .arrow {
transform: rotate(180deg);
}
`;
shadow.appendChild(style);
shadow.appendChild(content);
this._header = shadow.querySelector('.accordion-header');
this._item = shadow.querySelector('.accordion-item');
}
connectedCallback() {
this._header.addEventListener('click', () => this.toggle());
}
toggle() {
const isOpen = this._item.classList.contains('open');
if (isOpen) {
this.close();
} else {
this.open();
}
}
open() {
this._item.classList.add('open');
this.dispatchEvent(new CustomEvent('accordion-opened', {
bubbles: true,
composed: true
}));
}
close() {
this._item.classList.remove('open');
}
static get observedAttributes() {
return ['open'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'open') {
if (newValue !== null && this._item) {
this.open();
}
}
}
}
customElements.define('accordion-item', AccordionItem);
</script>
在HTML中使用折叠面板:
<accordion-item open>
<span slot="header">什么是Web Components?</span>
<p>Web Components是一套浏览器原生API,用于创建可复用的自定义元素。</p>
</accordion-item>
<accordion-item>
<span slot="header">它有哪些优势?</span>
<ul>
<li>框架无关</li>
<li>样式隔离</li>
<li>原生浏览器支持</li>
</ul>
</accordion-item>
<script>
// 监听折叠面板的打开事件
document.addEventListener('accordion-opened', (e) => {
console.log('面板已展开:', e.target);
});
</script>
这个折叠面板组件展示了Shadow DOM的样式封装能力、Custom Events的通信机制以及属性的响应式处理。每个<accordion-item>都是一个独立的作用域,互不干扰。
六、在React和Vue中使用Web Components
Web Components的精髓在于框架无关性。在React中使用自定义元素时,需要注意React对属性和事件的传递方式略有不同。
6.1 在React中使用
import React, { useRef, useEffect } from 'react';
function App() {
const ratingRef = useRef(null);
useEffect(() => {
const el = ratingRef.current;
const handler = (e) => {
console.log('评分改变:', e.detail.value);
};
el.addEventListener('rate-change', handler);
return () => el.removeEventListener('rate-change', handler);
}, []);
return (
<div>
<rating-card
ref={ratingRef}
initial-rating="4"
>
<span slot="title">React中的评分</span>
<span slot="comment">完美集成</span>
</rating-card>
</div>
);
}
6.2 在Vue中使用
<template>
<rating-card
initial-rating="5"
@rate-change="handleRateChange"
>
<span slot="title">Vue中的评分</span>
<span slot="comment">原生兼容</span>
</rating-card>
</template>
<script setup>
function handleRateChange(e) {
console.log('评分改变:', e.detail.value);
}
</script>
Vue对Web Components的支持尤为出色,可以直接使用v-model风格的绑定,自定义事件也能通过@语法监听。这种无缝集成让组件库的跨框架共享成为现实。
七、最佳实践与性能调优
- 使用语义化标签名:自定义元素名称必须包含连字符,建议使用有意义的命名如<data-table>、<modal-dialog>。
- 避免过度嵌套Shadow DOM:虽然Shadow DOM提供了强大的隔离,但过多嵌套会增加渲染开销。合理规划组件的粒度。
- 属性与属性反射:对于需要被JavaScript访问的属性,应在observedAttributes中列出,并在attributeChangedCallback中处理变更。
- 使用CSS自定义属性暴露样式接口:在Shadow DOM中使用var(–my-component-color),允许外部通过–my-component-color定制内部样式。
- 定期检查浏览器支持:Web Components在主流浏览器中已获得广泛支持,包括Chrome、Firefox、Safari和Edge。对于老旧浏览器,可使用polyfill。
八、总结
Web Components代表了Web平台对组件化开发的原生承诺。通过Custom Elements,我们能够定义自己的HTML标签;通过Shadow DOM,我们获得了彻底的样式隔离;通过Templates和Slots,我们拥有了灵活的内容分发机制;通过Custom Events,组件可以与外部世界进行通信。这些技术共同构成了一个完整的组件化方案,无需依赖任何第三方框架。
本文从基础概念到完整实战,覆盖了评分卡片和折叠面板两个实用组件的构建过程,并展示了它们在React和Vue中的使用方式。现在,你已经具备了创建自己的跨框架组件库的能力。开始在你的项目中尝试Web Components,享受一次编写、到处使用的自由吧。

