Web 组件化开发
什么是 Web Components
Web Components 是一组 W3C 标准,允许开发者创建可复用的自定义 HTML 元素。与 React 组件或 Vue 组件不同,它们是浏览器原生支持的,不依赖任何框架。
Web Components 由三项核心技术组成:
- Custom Elements:自定义元素
- Shadow DOM:影子 DOM,封装样式和结构
- HTML Templates:HTML 模板
Custom Elements
创建自定义元素
class MyButton extends HTMLElement {
constructor() {
super();
this.addEventListener('click', () => {
alert('按钮被点击!');
});
}
// 监听属性变化
static get observedAttributes() {
return ['disabled', 'variant'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'disabled') {
this.style.opacity = newValue !== null ? '0.5' : '1';
this.style.cursor = newValue !== null ? 'not-allowed' : 'pointer';
}
}
// 元素被插入 DOM
connectedCallback() {
this.render();
}
render() {
this.textContent = this.getAttribute('label') || '按钮';
}
}
// 注册自定义元素
customElements.define('my-button', MyButton);
<!-- 使用 -->
<my-button label="提交" disabled></my-button>
<my-button label="取消" variant="outline"></my-button>
生命周期回调
| 回调 | 触发时机 |
|---|---|
| constructor | 元素实例化时 |
| connectedCallback | 元素被插入 DOM 时 |
| disconnectedCallback | 元素从 DOM 移除时 |
| attributeChangedCallback | 监听属性变化时 |
| adoptedCallback | 元素被移动到新文档时 |
Shadow DOM
Shadow DOM 提供样式封装,组件内部的样式不会泄露到外部,外部样式也不会影响内部。
class MyCard extends HTMLElement {
constructor() {
super();
// 创建影子 DOM
this.attachShadow({ mode: 'open' }); // 或 'closed'
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
font-family: sans-serif;
}
.card-title {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 8px;
}
/* 这些样式不会影响外部元素 */
</style>
<div class="card">
<div class="card-title">
<slot name="title">默认标题</slot>
</div>
<div class="card-body">
<slot></slot> <!-- 默认插槽 -->
</div>
</div>
`;
}
}
customElements.define('my-card', MyCard);
<my-card>
<span slot="title">自定义标题</span>
<p>这是卡片内容,在 Shadow DOM 中渲染。</p>
</my-card>
HTML Templates
<template> 标签包含不会被渲染的 HTML,但可以被 JavaScript 实例化:
<template id="user-card-template">
<style>
.user-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 8px;
background: #f5f5f5;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: #007bff;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
}
</style>
<div class="user-card">
<div class="avatar"><slot name="avatar">👤</slot></div>
<div>
<div class="name"><slot name="name">用户名</slot></div>
<div class="email"><slot name="email">user@example.com</slot></div>
</div>
</div>
</template>
class UserCard extends HTMLElement {
constructor() {
super();
const template = document.getElementById('user-card-template');
const content = template.content.cloneNode(true);
this.attachShadow({ mode: 'open' }).appendChild(content);
}
}
customElements.define('user-card', UserCard);
实战:完整组件示例
class TodoList extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.todos = [];
}
connectedCallback() {
this.render();
this.shadowRoot.querySelector('.add-btn')
.addEventListener('click', () => this.addTodo());
}
addTodo() {
const input = this.shadowRoot.querySelector('.todo-input');
const text = input.value.trim();
if (!text) return;
this.todos.push({ id: Date.now(), text, done: false });
input.value = '';
this.renderTodos();
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
this.renderTodos();
}
render() {
this.shadowRoot.innerHTML = `
<style>
.container { font-family: sans-serif; max-width: 400px; }
.todo-input { padding: 8px; width: 70%; }
.add-btn { padding: 8px 16px; background: #007bff; color: white; border: none; cursor: pointer; }
.todo-item { display: flex; align-items: center; gap: 8px; padding: 8px 0; }
.done { text-decoration: line-through; color: #999; }
</style>
<div class="container">
<h3>待办事项</h3>
<div>
<input class="todo-input" placeholder="添加待办...">
<button class="add-btn">添加</button>
</div>
<div class="todo-list"></div>
</div>
`;
}
renderTodos() {
const list = this.shadowRoot.querySelector('.todo-list');
list.innerHTML = this.todos.map(todo => `
<div class="todo-item">
<input type="checkbox" ${todo.done ? 'checked' : ''} data-id="${todo.id}">
<span class="${todo.done ? 'done' : ''}">${todo.text}</span>
</div>
`).join('');
list.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', () => this.toggleTodo(Number(cb.dataset.id)));
});
}
}
customElements.define('todo-list', TodoList);
与框架的对比
| 特性 | Web Components | React/Vue 组件 |
|---|---|---|
| 框架依赖 | 无 | 需要框架运行时 |
| 浏览器兼容 | 需 polyfill | 框架处理 |
| 状态管理 | 手动管理 | 内置响应式系统 |
| 生态工具 | 有限 | 丰富 |
| 跨框架复用 | 天然支持 | 需封装 |
| 数据绑定 | 手动 | 自动 |
总结
Web Components 适合构建跨框架、长期维护的 UI 组件库。虽然开发体验不如主流框架便捷,但它们是浏览器原生标准,没有框架锁定风险。在实际项目中,可以根据需要选择纯 Web Components 或将其作为框架组件的补充。