Web Components是W3C标准化的浏览器原生组件技术,通过Custom Elements、Shadow DOM和HTML Templates三套规范实现样式与逻辑的完整封装。与React/Vue等框架组件不同,Web Components不依赖任何运行时框架,可在任意框架或纯HTML页面中直接使用。本文演示从自定义元素定义、Shadow DOM样式隔离到跨框架集成的完整开发流程。
Custom Elements自定义元素定义
自定义元素通过继承HTMLElement或其子类创建,生命周期回调包括connectedCallback(插入DOM)、disconnectedCallback(移除DOM)、attributeChangedCallback(属性变更)和adoptedCallback(跨文档移动)。实现一个带数据绑定的任务卡片组件:
class TaskCard extends HTMLElement {
static get observedAttributes() {
return ['title', 'priority', 'assignee', 'status'];
}
constructor() {
super();
this._shadow = this.attachShadow({ mode: 'open' });
this._data = {};
this._render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this._data[name] = newValue;
this._updateView(name);
}
}
connectedCallback() {
this._setupEventListeners();
}
disconnectedCallback() {
this._cleanup();
}
_render() {
this._shadow.innerHTML = `
<style>
:host {
display: block;
font-family: system-ui, sans-serif;
border-radius: 8px;
overflow: hidden;
margin-bottom: 12px;
}
.card {
padding: 16px;
border: 1px solid #e0e0e0;
border-left: 4px solid #ccc;
border-radius: 8px;
background: #fff;
transition: box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.card[data-priority="high"] { border-left-color: #e53935; }
.card[data-priority="medium"] { border-left-color: #fb8c00; }
.card[data-priority="low"] { border-left-color: #43a047; }
.title {
font-size: 15px;
font-weight: 600;
margin: 0 0 8px;
color: #1a1a1a;
}
.meta {
display: flex;
gap: 12px;
font-size: 13px;
color: #666;
}
.status-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-badge[data-status="todo"] { background: #e3f2fd; color: #1565c0; }
.status-badge[data-status="doing"] { background: #fff3e0; color: #e65100; }
.status-badge[data-status="done"] { background: #e8f5e9; color: #2e7d32; }
.btn-delete {
float: right;
border: none;
background: transparent;
cursor: pointer;
color: #999;
font-size: 18px;
padding: 0 4px;
}
.btn-delete:hover { color: #e53935; }
</style>
<div class="card">
<button class="btn-delete">×</button>
<h3 class="title">${this._data.title || 'Untitled'}</h3>
<div class="meta">
<span>Priority: ${this._data.priority || 'low'}</span>
<span>Assignee: ${this._data.assignee || 'Unassigned'}</span>
<span class="status-badge" data-status="${this._data.status || 'todo'}">${this._data.status || 'todo'}</span>
</div>
</div>
`;
}
_updateView(attr) {
const root = this._shadow.querySelector('.card');
if (!root) return;
if (attr === 'priority') {
root.dataset.priority = this._data.priority || 'low';
}
if (attr === 'status') {
const badge = root.querySelector('.status-badge');
if (badge) {
badge.dataset.status = this._data.status || 'todo';
badge.textContent = this._data.status || 'todo';
}
}
if (attr === 'title') {
const title = root.querySelector('.title');
if (title) title.textContent = this._data.title || 'Untitled';
}
}
_setupEventListeners() {
const deleteBtn = this._shadow.querySelector('.btn-delete');
this._deleteHandler = (e) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent('task-delete', {
detail: { id: this.id, title: this._data.title },
bubbles: true,
composed: true
}));
};
if (deleteBtn) {
deleteBtn.addEventListener('click', this._deleteHandler);
}
}
_cleanup() {
if (this._deleteHandler) {
const deleteBtn = this._shadow.querySelector('.btn-delete');
if (deleteBtn) {
deleteBtn.removeEventListener('click', this._deleteHandler);
}
}
}
}
customElements.define('task-card', TaskCard);
CustomEvent的composed: true允许事件穿越Shadow DOM边界,使外部框架能够监听组件内部触发的自定义事件。
Shadow DOM样式隔离机制
Shadow DOM创建独立的样式作用域,外部CSS无法穿透Shadow边界影响内部元素,内部样式也不会泄漏到外部文档。:host选择器用于设置组件宿主元素的样式:
/* 外部样式 - 不影响Shadow DOM内部 */
.card {
color: red; /* 无效,无法穿透Shadow DOM */
}
/* 组件内部 :host 伪类 */
:host {
--card-bg: #ffffff;
--card-border: #e0e0e0;
display: block;
contain: content;
}
:host([theme="dark"]) {
--card-bg: #1e1e1e;
--card-border: #333;
}
.card {
background: var(--card-bg);
border-color: var(--card-border);
}
通过CSS自定义属性(CSS Variables)可实现外部主题注入,这是跨Shadow DOM传递样式的标准机制。外部框架只需设置CSS变量值即可控制组件外观:
/* 外部框架控制主题 */
task-card {
--card-bg: #f5f5f5;
--card-border: #ddd;
}
task-card[theme="dark"] {
--card-bg: #1a1a2e;
--card-border: #333;
}
HTML Templates与Slot内容分发
使用template元素定义组件模板,通过slot实现内容分发,使组件支持外部内容嵌入:
const template = document.createElement('template');
template.innerHTML = `
<style>
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 16px; }
.panel-header { font-weight: 600; margin-bottom: 12px; }
.panel-body { font-size: 14px; line-height: 1.6; }
::slotted(h2) { margin: 0; font-size: 16px; }
</style>
<div class="panel">
<div class="panel-header">
<slot name="header">Default Header</slot>
</div>
<div class="panel-body">
<slot>Default content</slot>
</div>
</div>
`;
class ContentPanel extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('content-panel', ContentPanel);
使用方式,slot属性匹配对应插槽:
<content-panel>
<h2 slot="header">API网关配置指南</h2>
<p>API网关通过路由规则将请求分发到后端微服务...</p>
<p>支持限流、认证、日志等横切关注点...</p>
</content-panel>
跨框架集成与React/Vue桥接
React对Web Components的属性传递需要特殊处理,React 19+原生支持自定义元素属性绑定;React 18及以下版本需要通过ref手动设置属性:
// React 18 桥接方案
import { useRef, useEffect } from 'react';
function TaskList({ tasks, onDelete }) {
const containerRef = useRef(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleDelete = (e) => {
onDelete(e.detail.id);
};
container.addEventListener('task-delete', handleDelete);
return () => container.removeEventListener('task-delete', handleDelete);
}, [onDelete]);
return (
<div ref={containerRef}>
{tasks.map(task => (
<task-card
key={task.id}
id={task.id}
ref={el => {
if (el) {
el.setAttribute('title', task.title);
el.setAttribute('priority', task.priority);
el.setAttribute('assignee', task.assignee);
el.setAttribute('status', task.status);
}
}}
/>
))}
</div>
);
}
Vue 3通过v-is或直接使用自定义元素标签名即可集成,需在vue.config中配置compilerOptions.isCustomElement:
// vite.config.js
export default {
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.includes('-')
}
}
})
]
}
<!-- Vue 3 SFC -->
<template>
<div class="task-board">
<task-card
v-for="task in tasks"
:key="task.id"
:title="task.title"
:priority="task.priority"
:assignee="task.assignee"
:status="task.status"
@task-delete="handleDelete"
/>
</div>
</template>
Vue通过属性绑定自动将props传递给Web Components,事件监听器通过@前缀即可绑定自定义事件。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/webcomponents-kua-kuang-jia-zu-jian-kai-fa-shi-zhan-zi-ding/