Web Components是一套用于创建可复用自定义组件的Web标准技术。本章将介绍Web Components的基本概念和核心技术。
const webComponentsTech = {
CustomElements: {
description: '自定义元素',
purpose: '定义新的HTML标签'
},
ShadowDOM: {
description: '影子DOM',
purpose: '封装组件内部结构'
},
HTMLTemplates: {
description: 'HTML模板',
purpose: '定义可复用的HTML片段'
}
};
function checkSupport() {
return {
customElements: 'customElements' in window,
shadowDOM: 'attachShadow' in Element.prototype,
templates: 'content' in document.createElement('template'),
all: 'customElements' in window &&
'attachShadow' in Element.prototype &&
'content' in document.createElement('template')
};
}
class MyButton extends HTMLElement {
constructor() {
super();
this.addEventListener('click', () => {
console.log('按钮被点击');
});
}
connectedCallback() {
this.innerHTML = `<button>点击我</button>`;
}
}
customElements.define('my-button', MyButton);
const button = document.createElement('my-button');
document.body.appendChild(button);
document.body.innerHTML += '<my-button></my-button>';
class LifecycleElement extends HTMLElement {
constructor() {
super();
console.log('1. constructor - 元素被创建');
}
connectedCallback() {
console.log('2. connectedCallback - 元素被插入DOM');
}
disconnectedCallback() {
console.log('3. disconnectedCallback - 元素从DOM移除');
}
adoptedCallback() {
console.log('4. adoptedCallback - 元素被移动到新文档');
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`5. attributeChangedCallback - 属性 ${name} 从 ${oldValue} 变为 ${newValue}`);
}
static get observedAttributes() {
return ['name', 'count'];
}
}
customElements.define('lifecycle-element', LifecycleElement);
class UserCard extends HTMLElement {
static get observedAttributes() {
return ['name', 'email', 'avatar'];
}
get name() {
return this.getAttribute('name') || '';
}
set name(value) {
this.setAttribute('name', value);
}
get email() {
return this.getAttribute('email') || '';
}
set email(value) {
this.setAttribute('email', value);
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
connectedCallback() {
this.render();
}
render() {
this.innerHTML = `
<div class="user-card">
<img src="${this.avatar}" alt="${this.name}">
<h3>${this.name}</h3>
<p>${this.email}</p>
</div>
`;
}
}
customElements.define('user-card', UserCard);
class ShadowButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
<button><slot>按钮</slot></button>
`;
}
}
customElements.define('shadow-button', ShadowButton);
const shadowModes = {
open: {
description: '开放的Shadow DOM',
access: '可以通过element.shadowRoot访问',
use: '大多数情况下的选择'
},
closed: {
description: '封闭的Shadow DOM',
access: 'element.shadowRoot返回null',
use: '需要严格封装的场景'
}
};
class OpenShadow extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
}
class ClosedShadow extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: 'closed' });
}
get shadowRoot() {
return this._shadowRoot;
}
}
class StyledCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
margin: 16px;
}
:host(.highlighted) {
border-color: #007bff;
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.2);
}
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
:host-context(.dark-theme) {
background: #333;
color: white;
}
.title {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
::slotted(h2) {
color: #007bff;
}
::slotted(*) {
margin: 0;
}
</style>
<div class="title">
<slot name="title">默认标题</slot>
</div>
<div class="content">
<slot>默认内容</slot>
</div>
`;
}
}
customElements.define('styled-card', StyledCard);
class CardElement extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.card { border: 1px solid #ddd; padding: 16px; }
.header { border-bottom: 1px solid #eee; padding-bottom: 8px; }
.footer { border-top: 1px solid #eee; padding-top: 8px; }
</style>
<div class="card">
<div class="header">
<slot name="header">默认头部</slot>
</div>
<div class="body">
<slot>默认内容</slot>
</div>
<div class="footer">
<slot name="footer">默认底部</slot>
</div>
</div>
`;
}
}
customElements.define('card-element', CardElement);
<card-element>
<h2 slot="header">卡片标题</h2>
<p>这是卡片内容</p>
<span slot="footer">卡片底部</span>
</card-element>
class SlotContainer extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<slot id="content"></slot>
`;
this.slotElement = shadow.querySelector('slot');
}
connectedCallback() {
this.slotElement.addEventListener('slotchange', (e) => {
const nodes = this.slotElement.assignedNodes();
console.log('插槽内容变化:', nodes);
});
}
getSlotContent() {
return this.slotElement.assignedNodes();
}
getSlotElements() {
return this.slotElement.assignedElements();
}
}
<template id="card-template">
<style>
.card {
border: 1px solid #ccc;
padding: 16px;
border-radius: 8px;
}
</style>
<div class="card">
<h3 class="title"></h3>
<p class="content"></p>
</div>
</template>
<script>
class TemplateCard extends HTMLElement {
constructor() {
super();
const template = document.getElementById('card-template');
const content = template.content.cloneNode(true);
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(content);
}
connectedCallback() {
const title = this.getAttribute('title') || '标题';
const content = this.getAttribute('content') || '内容';
this.shadowRoot.querySelector('.title').textContent = title;
this.shadowRoot.querySelector('.content').textContent = content;
}
}
customElements.define('template-card', TemplateCard);
</script>
<template id="panel-template">
<style>
.panel { border: 1px solid #ddd; padding: 16px; }
</style>
<div class="panel">
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
</template>
class TodoList extends HTMLElement {
static get observedAttributes() {
return ['title'];
}
constructor() {
super();
this.todos = [];
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
setupEventListeners() {
const input = this.shadowRoot.querySelector('input');
const addButton = this.shadowRoot.querySelector('.add-btn');
const list = this.shadowRoot.querySelector('ul');
addButton.addEventListener('click', () => {
const text = input.value.trim();
if (text) {
this.addTodo(text);
input.value = '';
}
});
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addButton.click();
}
});
list.addEventListener('click', (e) => {
if (e.target.classList.contains('delete-btn')) {
const index = parseInt(e.target.dataset.index);
this.removeTodo(index);
} else if (e.target.classList.contains('todo-item')) {
const index = parseInt(e.target.dataset.index);
this.toggleTodo(index);
}
});
}
addTodo(text) {
this.todos.push({ text, done: false });
this.render();
this.emitChange();
}
removeTodo(index) {
this.todos.splice(index, 1);
this.render();
this.emitChange();
}
toggleTodo(index) {
this.todos[index].done = !this.todos[index].done;
this.render();
this.emitChange();
}
emitChange() {
this.dispatchEvent(new CustomEvent('change', {
detail: { todos: this.todos },
bubbles: true
}));
}
render() {
const title = this.getAttribute('title') || '待办事项';
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
font-family: Arial, sans-serif;
max-width: 400px;
}
h2 { margin: 0 0 16px; }
.input-group {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
input {
flex: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.add-btn {
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
display: flex;
align-items: center;
padding: 8px;
border-bottom: 1px solid #eee;
}
.todo-item {
flex: 1;
cursor: pointer;
}
.todo-item.done {
text-decoration: line-through;
color: #999;
}
.delete-btn {
padding: 4px 8px;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
</style>
<h2>${title}</h2>
<div class="input-group">
<input type="text" placeholder="添加新任务...">
<button class="add-btn">添加</button>
</div>
<ul>
${this.todos.map((todo, index) => `
<li>
<span class="todo-item ${todo.done ? 'done' : ''}"
data-index="${index}">
${todo.text}
</span>
<button class="delete-btn" data-index="${index}">删除</button>
</li>
`).join('')}
</ul>
`;
this.setupEventListeners();
}
}
customElements.define('todo-list', TodoList);
🧩 Web Components优势
- 原生支持:无需框架,浏览器原生支持
- 封装性好:Shadow DOM提供样式和结构封装
- 可复用:可在任何项目中使用
- 互操作性:与任何框架配合使用
📝 开发建议
- 使用语义化的自定义元素名称(包含连字符)
- 合理使用open和closed模式
- 提供良好的属性和事件接口
- 考虑无障碍访问支持
下一章将探讨 自定义元素进阶,学习更多自定义元素的高级特性。