Search K
Appearance
Appearance
关键词: Web Components, Custom Elements, Shadow DOM, HTML Templates, HTML Imports, 组件化开发, 封装性, 可复用性
Web Components是一套不同的技术,允许开发者创建可重用的自定义元素,并且在Web应用中使用它们。这些技术提供了一种标准化的方式来创建封装的、可复用的组件。
<!-- Web Components基础示例 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Web Components入门</title>
</head>
<body>
<h1>Web Components示例</h1>
<!-- 使用自定义元素 -->
<my-button type="primary" size="large">点击我</my-button>
<my-card title="卡片标题">
<p>这是卡片的内容</p>
</my-card>
<script>
// 简单的自定义元素示例
class MyButton extends HTMLElement {
constructor() {
super();
this.render();
}
render() {
const type = this.getAttribute('type') || 'default';
const size = this.getAttribute('size') || 'medium';
this.innerHTML = `
<style>
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn.primary {
background-color: #007bff;
color: white;
}
.btn.primary:hover {
background-color: #0056b3;
}
.btn.large {
padding: 12px 24px;
font-size: 16px;
}
</style>
<button class="btn ${type} ${size}">
<slot></slot>
</button>
`;
}
}
// 注册自定义元素
customElements.define('my-button', MyButton);
// 卡片组件示例
class MyCard extends HTMLElement {
constructor() {
super();
this.render();
}
render() {
const title = this.getAttribute('title') || '默认标题';
this.innerHTML = `
<style>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 12px;
color: #333;
}
.card-content {
color: #666;
line-height: 1.5;
}
</style>
<div class="card">
<div class="card-title">${title}</div>
<div class="card-content">
<slot></slot>
</div>
</div>
`;
}
}
customElements.define('my-card', MyCard);
</script>
</body>
</html>Web Components由四项主要技术组成:
Custom Elements允许开发者定义全新的HTML元素类型。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Custom Elements详解</title>
</head>
<body>
<h1>Custom Elements示例</h1>
<!-- 使用自定义元素 -->
<user-profile
name="张三"
email="zhangsan@example.com"
avatar="https://via.placeholder.com/100">
</user-profile>
<todo-list></todo-list>
<script>
// 用户资料组件
class UserProfile extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.render();
}
// 观察的属性
static get observedAttributes() {
return ['name', 'email', 'avatar'];
}
// 属性变化时调用
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
// 元素插入DOM时调用
connectedCallback() {
console.log('用户资料组件已连接到DOM');
this.addEventListener('click', this.handleClick);
}
// 元素从DOM移除时调用
disconnectedCallback() {
console.log('用户资料组件已从DOM移除');
this.removeEventListener('click', this.handleClick);
}
handleClick() {
const name = this.getAttribute('name');
alert(`点击了${name}的资料`);
}
render() {
const name = this.getAttribute('name') || '未知用户';
const email = this.getAttribute('email') || '未知邮箱';
const avatar = this.getAttribute('avatar') || 'https://via.placeholder.com/100';
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
cursor: pointer;
transition: box-shadow 0.3s ease;
}
:host(:hover) {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.profile {
display: flex;
align-items: center;
gap: 16px;
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
}
.info h3 {
margin: 0 0 8px 0;
color: #333;
}
.info p {
margin: 0;
color: #666;
font-size: 14px;
}
</style>
<div class="profile">
<img src="${avatar}" alt="${name}" class="avatar">
<div class="info">
<h3>${name}</h3>
<p>${email}</p>
</div>
</div>
`;
}
}
// 待办事项列表组件
class TodoList extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.todos = [];
this.render();
this.bindEvents();
}
connectedCallback() {
// 从本地存储加载数据
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
this.todos = JSON.parse(savedTodos);
this.render();
}
}
bindEvents() {
this.shadowRoot.addEventListener('click', (e) => {
if (e.target.classList.contains('add-btn')) {
this.addTodo();
} else if (e.target.classList.contains('delete-btn')) {
const index = parseInt(e.target.dataset.index);
this.deleteTodo(index);
} else if (e.target.classList.contains('todo-checkbox')) {
const index = parseInt(e.target.dataset.index);
this.toggleTodo(index);
}
});
this.shadowRoot.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && e.target.classList.contains('todo-input')) {
this.addTodo();
}
});
}
addTodo() {
const input = this.shadowRoot.querySelector('.todo-input');
const text = input.value.trim();
if (text) {
this.todos.push({
id: Date.now(),
text: text,
completed: false
});
input.value = '';
this.saveTodos();
this.render();
}
}
deleteTodo(index) {
this.todos.splice(index, 1);
this.saveTodos();
this.render();
}
toggleTodo(index) {
this.todos[index].completed = !this.todos[index].completed;
this.saveTodos();
this.render();
}
saveTodos() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
max-width: 400px;
margin: 16px 0;
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
}
.header {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.todo-input {
flex: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.add-btn {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-btn:hover {
background-color: #0056b3;
}
.todo-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.todo-item:last-child {
border-bottom: none;
}
.todo-checkbox {
cursor: pointer;
}
.todo-text {
flex: 1;
transition: all 0.3s ease;
}
.todo-text.completed {
text-decoration: line-through;
color: #999;
}
.delete-btn {
padding: 4px 8px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.delete-btn:hover {
background-color: #c82333;
}
.empty-state {
text-align: center;
color: #999;
padding: 32px;
}
</style>
<div class="header">
<input type="text" class="todo-input" placeholder="输入待办事项...">
<button class="add-btn">添加</button>
</div>
<div class="todo-list">
${this.todos.length === 0 ?
'<div class="empty-state">暂无待办事项</div>' :
this.todos.map((todo, index) => `
<div class="todo-item">
<input type="checkbox" class="todo-checkbox" data-index="${index}" ${todo.completed ? 'checked' : ''}>
<span class="todo-text ${todo.completed ? 'completed' : ''}">${todo.text}</span>
<button class="delete-btn" data-index="${index}">删除</button>
</div>
`).join('')
}
</div>
`;
}
}
// 注册自定义元素
customElements.define('user-profile', UserProfile);
customElements.define('todo-list', TodoList);
</script>
</body>
</html>Shadow DOM提供了一种将DOM和CSS封装在组件内部的方法,避免样式冲突。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Shadow DOM详解</title>
<style>
/* 全局样式 */
.button {
background-color: red;
color: white;
padding: 20px;
}
h3 {
color: blue;
}
</style>
</head>
<body>
<h1>Shadow DOM封装示例</h1>
<!-- 全局样式影响的元素 -->
<div class="button">全局样式按钮</div>
<h3>全局样式标题</h3>
<!-- 使用Shadow DOM的组件 -->
<modal-dialog title="确认对话框">
<p>您确定要删除这个项目吗?</p>
</modal-dialog>
<progress-bar value="75" max="100"></progress-bar>
<script>
// 模态对话框组件
class ModalDialog extends HTMLElement {
constructor() {
super();
// 创建Shadow DOM
this.attachShadow({ mode: 'open' });
this.render();
this.bindEvents();
}
static get observedAttributes() {
return ['title', 'open'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'title' && oldValue !== newValue) {
this.render();
}
}
bindEvents() {
this.shadowRoot.addEventListener('click', (e) => {
if (e.target.classList.contains('close-btn') ||
e.target.classList.contains('overlay')) {
this.close();
}
if (e.target.classList.contains('confirm-btn')) {
this.dispatchEvent(new CustomEvent('confirm', {
detail: { confirmed: true }
}));
this.close();
}
if (e.target.classList.contains('cancel-btn')) {
this.dispatchEvent(new CustomEvent('cancel', {
detail: { confirmed: false }
}));
this.close();
}
});
// 监听ESC键
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.hasAttribute('open')) {
this.close();
}
});
}
open() {
this.setAttribute('open', '');
document.body.style.overflow = 'hidden';
}
close() {
this.removeAttribute('open');
document.body.style.overflow = '';
}
render() {
const title = this.getAttribute('title') || '对话框';
const isOpen = this.hasAttribute('open');
this.shadowRoot.innerHTML = `
<style>
:host {
display: ${isOpen ? 'block' : 'none'};
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background-color: white;
border-radius: 8px;
padding: 0;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
animation: modalIn 0.3s ease-out;
}
@keyframes modalIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.modal-title {
margin: 0;
font-size: 18px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 20px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 20px;
border-top: 1px solid #eee;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.confirm-btn {
background-color: #dc3545;
color: white;
}
.confirm-btn:hover {
background-color: #c82333;
}
.cancel-btn {
background-color: #6c757d;
color: white;
}
.cancel-btn:hover {
background-color: #5a6268;
}
</style>
<div class="overlay">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">${title}</h3>
<button class="close-btn">×</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<button class="btn cancel-btn">取消</button>
<button class="btn confirm-btn">确认</button>
</div>
</div>
</div>
`;
}
}
// 进度条组件
class ProgressBar extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.render();
}
static get observedAttributes() {
return ['value', 'max'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
render() {
const value = parseFloat(this.getAttribute('value')) || 0;
const max = parseFloat(this.getAttribute('max')) || 100;
const percentage = Math.min((value / max) * 100, 100);
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
width: 100%;
margin: 16px 0;
}
.progress-container {
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
position: relative;
}
.progress-bar {
height: 100%;
background: linear-gradient(45deg, #4CAF50, #45a049);
width: ${percentage}%;
transition: width 0.3s ease;
position: relative;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #333;
font-size: 12px;
font-weight: bold;
}
.progress-info {
display: flex;
justify-content: space-between;
margin-top: 4px;
font-size: 12px;
color: #666;
}
</style>
<div class="progress-container">
<div class="progress-bar"></div>
<div class="progress-text">${percentage.toFixed(1)}%</div>
</div>
<div class="progress-info">
<span>进度: ${value}/${max}</span>
<span>${percentage.toFixed(1)}%</span>
</div>
`;
}
}
// 注册组件
customElements.define('modal-dialog', ModalDialog);
customElements.define('progress-bar', ProgressBar);
// 测试模态对话框
setTimeout(() => {
const modal = document.querySelector('modal-dialog');
modal.open();
modal.addEventListener('confirm', (e) => {
console.log('用户确认了操作');
});
modal.addEventListener('cancel', (e) => {
console.log('用户取消了操作');
});
}, 2000);
</script>
</body>
</html>HTML Templates和Slots提供了更灵活的内容组织方式。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>HTML Templates和Slots</title>
</head>
<body>
<h1>模板和插槽示例</h1>
<!-- 定义模板 -->
<template id="article-template">
<style>
.article {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}
.article-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.article-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin: 0;
}
.article-date {
color: #666;
font-size: 14px;
}
.article-content {
line-height: 1.6;
color: #555;
}
.article-footer {
margin-top: 16px;
padding-top: 8px;
border-top: 1px solid #eee;
}
.tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tag {
background-color: #f0f0f0;
color: #666;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
</style>
<article class="article">
<header class="article-header">
<h2 class="article-title">
<slot name="title">默认标题</slot>
</h2>
<time class="article-date">
<slot name="date">2024-01-01</slot>
</time>
</header>
<div class="article-content">
<slot>默认内容</slot>
</div>
<footer class="article-footer">
<div class="tags">
<slot name="tags"></slot>
</div>
</footer>
</article>
</template>
<!-- 使用带有插槽的组件 -->
<blog-article>
<span slot="title">Web Components深入解析</span>
<span slot="date">2024-01-15</span>
<p>Web Components是现代Web开发中的重要技术,它允许开发者创建可重用的自定义HTML元素。</p>
<p>本文将详细介绍Web Components的核心概念和实际应用。</p>
<span slot="tags" class="tag">Web Components</span>
<span slot="tags" class="tag">前端开发</span>
<span slot="tags" class="tag">HTML5</span>
</blog-article>
<blog-article>
<span slot="title">Shadow DOM的应用</span>
<span slot="date">2024-01-10</span>
<p>Shadow DOM为组件提供了样式和DOM封装,避免了全局样式冲突。</p>
<span slot="tags" class="tag">Shadow DOM</span>
<span slot="tags" class="tag">CSS</span>
</blog-article>
<!-- 复杂的卡片组件 -->
<feature-card>
<img slot="icon" src="https://via.placeholder.com/64" alt="图标">
<h3 slot="title">高性能</h3>
<p slot="description">使用最新的Web技术,提供卓越的性能体验</p>
<ul slot="features">
<li>快速加载</li>
<li>响应式设计</li>
<li>SEO友好</li>
</ul>
<button slot="action">了解更多</button>
</feature-card>
<script>
// 博客文章组件
class BlogArticle extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// 克隆模板
const template = document.getElementById('article-template');
const templateContent = template.content;
this.shadowRoot.appendChild(templateContent.cloneNode(true));
}
}
// 功能卡片组件
class FeatureCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
margin: 16px 0;
}
.card {
border: 1px solid #ddd;
border-radius: 12px;
padding: 24px;
text-align: center;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.icon {
margin-bottom: 16px;
}
.icon ::slotted(img) {
width: 64px;
height: 64px;
object-fit: cover;
}
.title {
margin-bottom: 12px;
}
.title ::slotted(h3) {
margin: 0;
color: #333;
font-size: 24px;
}
.description {
margin-bottom: 16px;
}
.description ::slotted(p) {
margin: 0;
color: #666;
line-height: 1.6;
}
.features {
margin-bottom: 20px;
}
.features ::slotted(ul) {
list-style: none;
padding: 0;
margin: 0;
}
.features ::slotted(li) {
padding: 4px 0;
color: #555;
position: relative;
}
.features ::slotted(li):before {
content: "✓";
color: #4CAF50;
font-weight: bold;
margin-right: 8px;
}
.action ::slotted(button) {
background-color: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
}
.action ::slotted(button):hover {
background-color: #0056b3;
}
</style>
<div class="card">
<div class="icon">
<slot name="icon"></slot>
</div>
<div class="title">
<slot name="title"></slot>
</div>
<div class="description">
<slot name="description"></slot>
</div>
<div class="features">
<slot name="features"></slot>
</div>
<div class="action">
<slot name="action"></slot>
</div>
</div>
`;
}
}
// 注册组件
customElements.define('blog-article', BlogArticle);
customElements.define('feature-card', FeatureCard);
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Web Components实践案例</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin: 40px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
</style>
</head>
<body>
<h1>Web Components实践案例</h1>
<div class="demo-section">
<h2>数据表格组件</h2>
<data-table id="users-table"></data-table>
</div>
<div class="demo-section">
<h2>表单组件</h2>
<form-builder id="contact-form"></form-builder>
</div>
<div class="demo-section">
<h2>通知组件</h2>
<button onclick="showNotification('success', '操作成功!')">成功通知</button>
<button onclick="showNotification('error', '操作失败!')">错误通知</button>
<button onclick="showNotification('warning', '警告信息')">警告通知</button>
<button onclick="showNotification('info', '提示信息')">信息通知</button>
</div>
<notification-container></notification-container>
<script>
// 数据表格组件
class DataTable extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.data = [];
this.columns = [];
this.sortColumn = null;
this.sortDirection = 'asc';
this.currentPage = 1;
this.pageSize = 5;
this.render();
}
connectedCallback() {
// 模拟数据加载
this.loadData();
}
async loadData() {
// 模拟API请求
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
this.data = users.map(user => ({
id: user.id,
name: user.name,
email: user.email,
phone: user.phone,
website: user.website
}));
this.columns = [
{ key: 'id', title: 'ID', sortable: true },
{ key: 'name', title: '姓名', sortable: true },
{ key: 'email', title: '邮箱', sortable: true },
{ key: 'phone', title: '电话', sortable: false },
{ key: 'website', title: '网站', sortable: false }
];
this.render();
}
sort(column) {
if (this.sortColumn === column) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortColumn = column;
this.sortDirection = 'asc';
}
this.data.sort((a, b) => {
const aValue = a[column];
const bValue = b[column];
if (aValue < bValue) return this.sortDirection === 'asc' ? -1 : 1;
if (aValue > bValue) return this.sortDirection === 'asc' ? 1 : -1;
return 0;
});
this.render();
}
changePage(page) {
this.currentPage = page;
this.render();
}
getPaginatedData() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.data.slice(start, end);
}
getTotalPages() {
return Math.ceil(this.data.length / this.pageSize);
}
render() {
const paginatedData = this.getPaginatedData();
const totalPages = this.getTotalPages();
this.shadowRoot.innerHTML = `
<style>
.table-container {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.table th {
background-color: #f8f9fa;
font-weight: bold;
cursor: pointer;
user-select: none;
}
.table th:hover {
background-color: #e9ecef;
}
.table th.sortable:after {
content: " ⇅";
color: #999;
}
.table th.sorted-asc:after {
content: " ↑";
color: #007bff;
}
.table th.sorted-desc:after {
content: " ↓";
color: #007bff;
}
.table tbody tr:hover {
background-color: #f8f9fa;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
padding: 16px;
background-color: #f8f9fa;
}
.pagination button {
padding: 8px 12px;
border: 1px solid #ddd;
background-color: white;
cursor: pointer;
border-radius: 4px;
}
.pagination button:hover {
background-color: #e9ecef;
}
.pagination button.active {
background-color: #007bff;
color: white;
border-color: #007bff;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<div class="table-container">
<table class="table">
<thead>
<tr>
${this.columns.map(col => `
<th class="${col.sortable ? 'sortable' : ''} ${this.sortColumn === col.key ? 'sorted-' + this.sortDirection : ''}"
onclick="this.getRootNode().host.sort('${col.key}')">
${col.title}
</th>
`).join('')}
</tr>
</thead>
<tbody>
${paginatedData.map(row => `
<tr>
${this.columns.map(col => `
<td>${row[col.key]}</td>
`).join('')}
</tr>
`).join('')}
</tbody>
</table>
<div class="pagination">
<button onclick="this.getRootNode().host.changePage(${this.currentPage - 1})"
${this.currentPage === 1 ? 'disabled' : ''}>
上一页
</button>
${Array.from({length: totalPages}, (_, i) => i + 1).map(page => `
<button class="${page === this.currentPage ? 'active' : ''}"
onclick="this.getRootNode().host.changePage(${page})">
${page}
</button>
`).join('')}
<button onclick="this.getRootNode().host.changePage(${this.currentPage + 1})"
${this.currentPage === totalPages ? 'disabled' : ''}>
下一页
</button>
</div>
</div>
`;
}
}
// 表单构建器组件
class FormBuilder extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.formData = {};
this.render();
this.bindEvents();
}
bindEvents() {
this.shadowRoot.addEventListener('submit', (e) => {
e.preventDefault();
this.handleSubmit();
});
this.shadowRoot.addEventListener('input', (e) => {
this.formData[e.target.name] = e.target.value;
});
}
handleSubmit() {
const isValid = this.validateForm();
if (isValid) {
console.log('表单数据:', this.formData);
showNotification('success', '表单提交成功!');
} else {
showNotification('error', '请填写所有必填字段!');
}
}
validateForm() {
const requiredFields = ['name', 'email', 'message'];
return requiredFields.every(field => this.formData[field] && this.formData[field].trim());
}
render() {
this.shadowRoot.innerHTML = `
<style>
.form {
max-width: 500px;
margin: 0 auto;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #333;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.form-textarea {
resize: vertical;
min-height: 100px;
}
.form-submit {
background-color: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
}
.form-submit:hover {
background-color: #0056b3;
}
.required {
color: #dc3545;
}
</style>
<form class="form">
<div class="form-group">
<label class="form-label">
姓名 <span class="required">*</span>
</label>
<input type="text" name="name" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">
邮箱 <span class="required">*</span>
</label>
<input type="email" name="email" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">主题</label>
<select name="subject" class="form-select">
<option value="">请选择主题</option>
<option value="general">一般询问</option>
<option value="support">技术支持</option>
<option value="feedback">意见反馈</option>
</select>
</div>
<div class="form-group">
<label class="form-label">
消息 <span class="required">*</span>
</label>
<textarea name="message" class="form-textarea" required></textarea>
</div>
<button type="submit" class="form-submit">提交表单</button>
</form>
`;
}
}
// 通知容器组件
class NotificationContainer extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.notifications = [];
this.render();
}
addNotification(type, message) {
const id = Date.now();
const notification = { id, type, message };
this.notifications.push(notification);
this.render();
// 自动移除通知
setTimeout(() => {
this.removeNotification(id);
}, 5000);
}
removeNotification(id) {
this.notifications = this.notifications.filter(n => n.id !== id);
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
.notification {
padding: 16px;
margin-bottom: 8px;
border-radius: 6px;
color: white;
min-width: 300px;
position: relative;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification.success {
background-color: #28a745;
}
.notification.error {
background-color: #dc3545;
}
.notification.warning {
background-color: #ffc107;
color: #212529;
}
.notification.info {
background-color: #17a2b8;
}
.close-btn {
position: absolute;
top: 8px;
right: 8px;
background: none;
border: none;
color: inherit;
cursor: pointer;
font-size: 18px;
padding: 0;
width: 24px;
height: 24px;
}
</style>
${this.notifications.map(notification => `
<div class="notification ${notification.type}">
${notification.message}
<button class="close-btn" onclick="this.getRootNode().host.removeNotification(${notification.id})">×</button>
</div>
`).join('')}
`;
}
}
// 注册组件
customElements.define('data-table', DataTable);
customElements.define('form-builder', FormBuilder);
customElements.define('notification-container', NotificationContainer);
// 全局通知函数
function showNotification(type, message) {
const container = document.querySelector('notification-container');
container.addNotification(type, message);
}
</script>
</body>
</html>A: Web Components是浏览器原生支持的标准,无需依赖框架;而React/Vue是JavaScript框架,提供了更丰富的生态系统和开发工具。
A: Shadow DOM创建了一个封闭的作用域,外部CSS无法影响内部元素,内部CSS也不会影响外部元素,实现了真正的样式隔离。
A: 可以使用addEventListener添加事件监听器,使用CustomEvent创建自定义事件,通过dispatchEvent分发事件。
A: 现代浏览器都支持Web Components,对于老旧浏览器可以使用polyfill来提供兼容性支持。
A: 可以使用传统的JavaScript测试框架(如Jest)结合DOM操作来测试Web Components的功能和行为。
下一节预览:下一节我们将学习Progressive Web Apps,重点介绍PWA的核心概念、Service Worker和离线功能实现。