Web Components
一、Web Components 概述
1.1 什么是 Web Components
Web Components 是一组 Web 平台 API 的统称,允许开发者创建可复用的自定义元素:
- Custom Elements:创建自定义 HTML 元素
- Shadow DOM:封装 DOM 和样式
- HTML Templates:模板内容的惰性解析
- Slots:内容分发机制
Web Components 的核心理念是可复用性和封装性——创建的组件可以在任何框架或原生 HTML 中使用。
1.2 与框架组件的区别
| 特性 | Web Components | React/Vue |
|---|---|---|
| 运行环境 | 原生浏览器 | 需要框架 |
| 样式封装 | Shadow DOM | CSS Modules/Styled |
| 生命周期 | 浏览器定义 | 框架定义 |
| 跨框架使用 | 原生支持 | 需要适配 |
| 生态 | 较小 | 丰富 |
二、Custom Elements
2.1 自定义元素的分类
Autonomous custom elements:独立元素,继承自 HTMLElement
class MyButton extends HTMLElement {
constructor() {
super();
// ...
}
}
customElements.define('my-button', MyButton);
Customized built-in elements:继承自现有元素
class MyButton extends HTMLButtonElement {
constructor() {
super();
// ...
}
}
customElements.define('my-button', MyButton, { extends: 'button' });
// 使用:<button is="my-button">Click</button>
2.2 生命周期回调
class MyElement extends HTMLElement {
constructor() {
super();
// 元素被创建时调用,但元素还未挂载到文档
console.log('constructor');
}
connectedCallback() {
// 元素首次被插入到文档时调用
console.log('connectedCallback');
}
disconnectedCallback() {
// 元素从文档中删除时调用
console.log('disconnectedCallback');
}
attributeChangedCallback(name, oldValue, newValue) {
// 元素的属性变化时调用
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
}
adoptedCallback() {
// 元素被移动到新文档时调用(极少使用)
console.log('adoptedCallback');
}
static get observedAttributes() {
// 返回需要观察的属性列表
return ['title', 'disabled'];
}
}
2.3 响应式属性
class CounterElement extends HTMLElement {
static get observedAttributes() {
return ['count'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._count = 0;
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'count') {
this._count = parseInt(newValue, 10) || 0;
this.update();
}
}
// 允许 JavaScript 属性访问
get count() {
return this._count;
}
set count(value) {
this.setAttribute('count', value);
}
update() {
const span = this.shadowRoot.querySelector('span');
span.textContent = this._count;
}
render() {
this.shadowRoot.innerHTML = `
<button id="dec">-</button>
<span>${this._count}</span>
<button id="inc">+</button>
`;
this.shadowRoot.querySelector('#inc').onclick = () => {
this.count++;
};
this.shadowRoot.querySelector('#dec').onclick = () => {
this.count--;
};
}
}
customElements.define('counter-element', CounterElement);
三、Shadow DOM
3.1 attachShadow 模式
// open 模式:可以从外部访问 shadowRoot
const shadow = element.attachShadow({ mode: 'open' });
// closed 模式:无法从外部访问
const shadow = element.attachShadow({ mode: 'closed' });
// shadowRoot 返回 null
建议使用 open 模式。closed 模式可以通过 Element.attachShadow() 的内部引用绕过,而且会阻止工具和框架访问。
3.2 Shadow DOM 的样式封装
class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
:host-context(.dark-theme) {
background: #333;
color: white;
}
.content {
color: inherit;
}
</style>
<div class="content">
<slot></slot>
</div>
`;
}
}
样式封装规则:
:host选择器:选择 Shadow DOM 的根元素:host([attr]):当元素有特定属性时匹配:host-context(selector):当祖先元素匹配选择器时匹配::slotted(selector):选择被分发到 slot 的元素
3.3 ::slotted() 伪元素
<!-- 组件定义 -->
<template id="my-card">
<style>
.header { background: #f0f0f0; padding: 10px; }
::slotted(h2) { margin: 0; color: blue; }
</style>
<div class="header">
<slot name="title"></slot>
</div>
<div class="body">
<slot></slot>
</div>
</template>
<!-- 使用组件 -->
<my-card>
<h2 slot="title">Card Title</h2>
<p>Card content</p>
</my-card>
四、HTML Templates
4.1 template 元素
<template id="my-template">
<style>
p { color: blue; }
</style>
<p>This is a template</p>
</template>
<script>
const template = document.getElementById('my-template');
const clone = template.content.cloneNode(true);
document.body.appendChild(clone);
</script>
template 的特点:
- 内容是惰性解析的,不会被执行或渲染
- 内部的脚本不会运行,样式不会应用
- 可以被多次克隆使用
4.2 与 customElements 结合
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const template = document.getElementById('my-card-template');
const clone = template.content.cloneNode(true);
this.shadowRoot.appendChild(clone);
// 绑定事件
this.shadowRoot.querySelector('.close').onclick = () => {
this.remove();
};
}
}
customElements.define('my-card', MyCard);
五、Slots 内容分发
5.1 匿名 slot
<!-- 组件 -->
<div class="container">
<slot></slot>
</div>
<!-- 使用 -->
<my-element>
<p>这段内容会分发到匿名 slot</p>
</my-element>
5.2 命名 slot
<!-- 组件 -->
<div class="card">
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</div>
<!-- 使用 -->
<my-card>
<h1 slot="header">Title</h1>
<p>Main content</p>
<span slot="footer">Footer content</span>
</my-card>
5.3 slotchange 事件
class MyElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `<slot></slot>`;
// 监听 slot 内容变化
this.shadowRoot.querySelector('slot').addEventListener('slotchange', (e) => {
const nodes = e.target.assignedNodes();
console.log(`Slot changed, ${nodes.length} nodes`);
});
}
}
六、与 React/Vue 的对比
6.1 Web Components 的优势
- 原生支持:不需要任何框架,可以在任何 HTML 中使用
- 真正的封装:Shadow DOM 提供真正的样式隔离
- 跨框架使用:同一个组件可以在 React、Vue、Angular 中使用
6.2 Web Components 的劣势
- 生态较小:没有 React/Vue 那么丰富的社区组件
- 开发体验:没有热更新、DevTools 调试等现代化工具
- 响应式系统:需要自己实现,没有框架的响应式系统方便
- 浏览器支持:旧浏览器需要 polyfills
6.3 混合使用
// 在 React 中使用 Web Component
class MyCard extends HTMLElement {
connectedCallback() {
this.innerHTML = `<div>Card Content</div>`;
}
}
customElements.define('my-card', MyCard);
// React 使用
function App() {
return <my-card />;
}