Web Components是一套不同的技术,允许创建可重用的自定义元素,它们的功能封装在代码之外,可以在Web应用中使用。
东巴文(db-w.cn) 认为:Web Components是Web平台原生的组件化解决方案,提供了真正的封装和复用能力。掌握Web Components,能构建跨框架的通用组件。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Components概述 - 东巴文</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
}
h1 {
color: #667eea;
margin-bottom: 20px;
}
h2 {
color: #764ba2;
margin: 30px 0 15px;
border-left: 4px solid #764ba2;
padding-left: 15px;
}
.component-demo {
background: #f8f9fa;
padding: 20px;
border: 2px solid #ddd;
margin: 15px 0;
border-radius: 10px;
}
.component-demo h3 {
color: #667eea;
margin-bottom: 10px;
}
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.feature-list li:before {
content: "✓ ";
color: #667eea;
font-weight: bold;
}
.tech-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.tech-table th,
.tech-table td {
padding: 12px;
text-align: left;
border: 1px solid #ddd;
}
.tech-table th {
background: #667eea;
color: white;
}
</style>
</head>
<body>
<div class="container">
<h1>Web Components概述</h1>
<h2>什么是Web Components</h2>
<div class="component-demo">
<h3>核心概念</h3>
<p>Web Components是一套Web平台API,允许创建可重用的自定义HTML元素。它们由三个主要技术组成:</p>
<ul class="feature-list">
<li><strong>Custom Elements(自定义元素):</strong> 定义新HTML元素的API</li>
<li><strong>Shadow DOM(影子DOM):</strong> 封装元素内部结构的API</li>
<li><strong>HTML Templates(HTML模板):</strong> 定义可重用模板的API</li>
</ul>
</div>
<h2>Web Components优势</h2>
<table class="tech-table">
<thead>
<tr>
<th>优势</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>原生支持</strong></td>
<td>浏览器原生支持,无需框架</td>
</tr>
<tr>
<td><strong>封装性</strong></td>
<td>Shadow DOM提供样式和结构的封装</td>
</tr>
<tr>
<td><strong>可复用</strong></td>
<td>组件可以在任何地方使用</td>
</tr>
<tr>
<td><strong>跨框架</strong></td>
<td>可以在Vue、React、Angular等框架中使用</td>
</tr>
<tr>
<td><strong>互操作性</strong></td>
<td>与现有HTML元素无缝集成</td>
</tr>
</tbody>
</table>
<h2>浏览器支持</h2>
<div class="component-demo">
<h3>支持情况</h3>
<div style="background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 5px; margin: 10px 0;">
<pre>
// 检查浏览器支持
const supportsCustomElements = 'customElements' in window;
const supportsShadowDOM = 'attachShadow' in Element.prototype;
const supportsTemplates = 'content' in document.createElement('template');
console.log('Custom Elements:', supportsCustomElements);
console.log('Shadow DOM:', supportsShadowDOM);
console.log('HTML Templates:', supportsTemplates);
// 所有现代浏览器都支持Web Components
// Chrome 67+, Firefox 63+, Safari 10.1+, Edge 79+
</pre>
</div>
</div>
<h2>简单示例</h2>
<div class="component-demo">
<h3>第一个Web Component</h3>
<div style="background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 5px; margin: 10px 0;">
<pre>
// 定义自定义元素
class MyComponent extends HTMLElement {
constructor() {
super();
// 创建Shadow DOM
const shadow = this.attachShadow({ mode: 'open' });
// 创建模板
const template = document.createElement('template');
template.innerHTML = `
<style>
.container {
padding: 20px;
background: #667eea;
color: white;
border-radius: 10px;
}
</style>
<div class="container">
<h3>我的第一个Web Component</h3>
<slot></slot>
</div>
`;
// 添加到Shadow DOM
shadow.appendChild(template.content.cloneNode(true));
}
}
// 注册自定义元素
customElements.define('my-component', MyComponent);
// 使用组件
// <my-component>这是组件内容</my-component>
</pre>
</div>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 5px; margin-top: 20px;">
<strong>东巴文提示:</strong>Web Components是Web平台的原生组件化方案,提供了真正的封装和复用能力。掌握Custom Elements、Shadow DOM和HTML Templates三大核心技术,能构建跨框架的通用组件。
</div>
</div>
</body>
</html>
自定义元素是Web Components的核心,允许创建新的HTML元素,扩展HTML词汇表。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义元素 - 东巴文</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
}
h1 {
color: #667eea;
margin-bottom: 20px;
}
h2 {
color: #764ba2;
margin: 30px 0 15px;
border-left: 4px solid #764ba2;
padding-left: 15px;
}
.element-demo {
background: #f8f9fa;
padding: 20px;
border: 2px solid #ddd;
margin: 15px 0;
border-radius: 10px;
}
.element-demo h3 {
color: #667eea;
margin-bottom: 10px;
}
.code-block {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>自定义元素</h1>
<h2>创建自定义元素</h2>
<div class="element-demo">
<h3>基本结构</h3>
<div class="code-block">
<pre>
// 创建自定义元素
class MyElement extends HTMLElement {
constructor() {
super();
// 初始化代码
}
// 生命周期回调
connectedCallback() {
// 元素被插入DOM时调用
console.log('元素已连接到DOM');
}
disconnectedCallback() {
// 元素从DOM移除时调用
console.log('元素已从DOM移除');
}
adoptedCallback() {
// 元素被移动到新文档时调用
console.log('元素已移动到新文档');
}
attributeChangedCallback(name, oldValue, newValue) {
// 属性变化时调用
console.log(`属性 ${name} 从 ${oldValue} 变为 ${newValue}`);
}
// 监听的属性
static get observedAttributes() {
return ['data-value', 'disabled'];
}
}
// 注册自定义元素
customElements.define('my-element', MyElement);
// 使用
// <my-element data-value="123"></my-element>
</pre>
</div>
</div>
<h2>自定义内置元素</h2>
<div class="element-demo">
<h3>扩展现有元素</h3>
<div class="code-block">
<pre>
// 扩展HTMLButtonElement
class MyButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', () => {
alert('自定义按钮被点击!');
});
}
connectedCallback() {
this.style.background = '#667eea';
this.style.color = 'white';
this.style.border = 'none';
this.style.padding = '10px 20px';
this.style.borderRadius = '5px';
this.style.cursor = 'pointer';
}
}
// 注册自定义内置元素
customElements.define('my-button', MyButton, { extends: 'button' });
// 使用
// <button is="my-button">点击我</button>
</pre>
</div>
</div>
<h2>属性和属性反射</h2>
<div class="element-demo">
<h3>属性处理</h3>
<div class="code-block">
<pre>
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.render();
}
// 定义属性
static get observedAttributes() {
return ['name', 'email', 'avatar'];
}
// Getter和Setter
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.shadowRoot.innerHTML = `
<style>
.card {
border: 1px solid #ddd;
padding: 20px;
border-radius: 10px;
display: flex;
align-items: center;
gap: 15px;
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background: #667eea;
}
.info h3 {
margin: 0 0 5px 0;
color: #333;
}
.info p {
margin: 0;
color: #666;
font-size: 14px;
}
</style>
<div class="card">
<img class="avatar" src="${this.getAttribute('avatar')}" alt="头像">
<div class="info">
<h3>${this.name}</h3>
<p>${this.email}</p>
</div>
</div>
`;
}
}
customElements.define('user-card', UserCard);
// 使用
// <user-card name="张三" email="zhangsan@example.com" avatar="avatar.jpg"></user-card>
</pre>
</div>
</div>
<h2>自定义元素方法</h2>
<div class="element-demo">
<h3>公共方法</h3>
<div class="code-block">
<pre>
class CounterElement extends HTMLElement {
constructor() {
super();
this._count = 0;
this.attachShadow({ mode: 'open' });
this.render();
}
// 公共方法
increment() {
this._count++;
this.render();
}
decrement() {
this._count--;
this.render();
}
reset() {
this._count = 0;
this.render();
}
// Getter
get count() {
return this._count;
}
render() {
this.shadowRoot.innerHTML = `
<style>
.counter {
display: flex;
align-items: center;
gap: 10px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
}
button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background: #764ba2;
}
.count {
font-size: 24px;
font-weight: bold;
min-width: 50px;
text-align: center;
}
</style>
<div class="counter">
<button id="dec">-</button>
<span class="count">${this._count}</span>
<button id="inc">+</button>
</div>
`;
// 绑定事件
this.shadowRoot.getElementById('inc').addEventListener('click', () => {
this.increment();
});
this.shadowRoot.getElementById('dec').addEventListener('click', () => {
this.decrement();
});
}
}
customElements.define('counter-element', CounterElement);
// 使用
// <counter-element id="myCounter"></counter-element>
// document.getElementById('myCounter').increment();
// document.getElementById('myCounter').reset();
</pre>
</div>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 5px; margin-top: 20px;">
<strong>东巴文提示:</strong>自定义元素是Web Components的核心。掌握生命周期回调、属性处理、公共方法等API,能创建功能完整的自定义组件。注意命名规范,必须包含连字符(-)。
</div>
</div>
</body>
</html>
Shadow DOM提供了真正的封装,将组件的内部结构隐藏起来,避免样式冲突。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shadow DOM - 东巴文</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
}
h1 {
color: #667eea;
margin-bottom: 20px;
}
h2 {
color: #764ba2;
margin: 30px 0 15px;
border-left: 4px solid #764ba2;
padding-left: 15px;
}
.shadow-demo {
background: #f8f9fa;
padding: 20px;
border: 2px solid #ddd;
margin: 15px 0;
border-radius: 10px;
}
.shadow-demo h3 {
color: #667eea;
margin-bottom: 10px;
}
.code-block {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>Shadow DOM</h1>
<h2>Shadow DOM基础</h2>
<div class="shadow-demo">
<h3>创建Shadow DOM</h3>
<div class="code-block">
<pre>
// 创建Shadow DOM
const element = document.querySelector('#myElement');
const shadow = element.attachShadow({ mode: 'open' });
// mode: 'open' - 可以通过element.shadowRoot访问
// mode: 'closed' - 无法访问shadowRoot
// 添加内容
shadow.innerHTML = `
<style>
.container {
padding: 20px;
background: #667eea;
color: white;
border-radius: 10px;
}
</style>
<div class="container">
<h3>Shadow DOM内容</h3>
<p>这些样式不会影响外部</p>
</div>
`;
</pre>
</div>
</div>
<h2>样式封装</h2>
<div class="shadow-demo">
<h3>样式隔离示例</h3>
<div class="code-block">
<pre>
class StyledComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
/* Shadow DOM内的样式 */
.button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.button:hover {
background: #764ba2;
}
/* 这些样式不会影响外部 */
h3 {
color: #667eea;
margin: 0 0 10px 0;
}
</style>
<div>
<h3>样式封装示例</h3>
<button class="button">Shadow DOM按钮</button>
</div>
`;
}
}
customElements.define('styled-component', StyledComponent);
// 外部样式不会影响Shadow DOM内部
// Shadow DOM内部样式也不会影响外部
</pre>
</div>
</div>
<h2>插槽(Slot)</h2>
<div class="shadow-demo">
<h3>内容分发</h3>
<div class="code-block">
<pre>
class CardComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.card {
border: 1px solid #ddd;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header {
background: #667eea;
color: white;
padding: 20px;
}
.body {
padding: 20px;
}
.footer {
background: #f8f9fa;
padding: 15px;
border-top: 1px solid #ddd;
}
</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-component', CardComponent);
// 使用
/*
<card-component>
<h2 slot="header">卡片标题</h2>
<p>这是卡片的主要内容</p>
<p>可以包含多个段落</p>
<button slot="footer">确定</button>
</card-component>
*/
</pre>
</div>
</div>
<h2>样式穿透</h2>
<div class="shadow-demo">
<h3>外部样式影响Shadow DOM</h3>
<div class="code-block">
<pre>
class ThemedComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host {
display: block;
padding: 20px;
background: var(--component-bg, #f8f9fa);
color: var(--component-color, #333);
border-radius: 10px;
}
:host(.dark) {
background: #333;
color: white;
}
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
/* 穿透到插槽内容 */
::slotted(h3) {
color: #667eea;
margin: 0 0 10px 0;
}
::slotted(p) {
line-height: 1.6;
}
</style>
<div>
<slot></slot>
</div>
`;
}
}
customElements.define('themed-component', ThemedComponent);
// 使用CSS变量
/*
<style>
themed-component {
--component-bg: #667eea;
--component-color: white;
}
</style>
<themed-component>
<h3>主题组件</h3>
<p>使用CSS变量自定义样式</p>
</themed-component>
<themed-component class="dark">
<h3>暗色主题</h3>
<p>通过class切换主题</p>
</themed-component>
*/
</pre>
</div>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 5px; margin-top: 20px;">
<strong>东巴文提示:</strong>Shadow DOM提供了真正的封装,避免样式冲突。使用:host选择器定义组件样式,使用::slotted()穿透插槽内容,使用CSS变量实现主题定制。合理使用open和closed模式。
</div>
</div>
</body>
</html>
HTML模板允许定义可重用的HTML结构,在需要时实例化。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML模板 - 东巴文</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
}
h1 {
color: #667eea;
margin-bottom: 20px;
}
h2 {
color: #764ba2;
margin: 30px 0 15px;
border-left: 4px solid #764ba2;
padding-left: 15px;
}
.template-demo {
background: #f8f9fa;
padding: 20px;
border: 2px solid #ddd;
margin: 15px 0;
border-radius: 10px;
}
.template-demo h3 {
color: #667eea;
margin-bottom: 10px;
}
.code-block {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>HTML模板</h1>
<h2>template元素</h2>
<div class="template-demo">
<h3>定义模板</h3>
<div class="code-block">
<pre>
<!-- 定义模板 -->
<template id="cardTemplate">
<style>
.card {
border: 1px solid #ddd;
border-radius: 10px;
padding: 20px;
margin: 10px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.card h3 {
color: #667eea;
margin: 0 0 10px 0;
}
.card p {
color: #666;
margin: 0;
}
</style>
<div class="card">
<h3></h3>
<p></p>
</div>
</template>
<!-- 使用模板 -->
<div id="cardContainer"></div>
<script>
// 获取模板
const template = document.getElementById('cardTemplate');
const container = document.getElementById('cardContainer');
// 克隆模板
const clone = template.content.cloneNode(true);
// 填充内容
clone.querySelector('h3').textContent = '卡片标题';
clone.querySelector('p').textContent = '这是卡片内容';
// 添加到DOM
container.appendChild(clone);
</script>
</pre>
</div>
</div>
<h2>slot元素</h2>
<div class="template-demo">
<h3>命名插槽</h3>
<div class="code-block">
<pre>
<!-- 定义带插槽的模板 -->
<template id="articleTemplate">
<style>
.article {
border: 1px solid #ddd;
border-radius: 10px;
overflow: hidden;
}
.header {
background: #667eea;
color: white;
padding: 20px;
}
.content {
padding: 20px;
}
.footer {
background: #f8f9fa;
padding: 15px;
border-top: 1px solid #ddd;
}
</style>
<article class="article">
<header class="header">
<slot name="title">默认标题</slot>
</header>
<div class="content">
<slot name="content">默认内容</slot>
</div>
<footer class="footer">
<slot name="footer">默认页脚</slot>
</footer>
</article>
</template>
<!-- 使用 -->
<custom-article>
<h2 slot="title">文章标题</h2>
<p slot="content">文章内容</p>
<small slot="footer">发布时间: 2024-01-01</small>
</custom-article>
</pre>
</div>
</div>
<h2>在Web Components中使用模板</h2>
<div class="template-demo">
<h3>模板与组件结合</h3>
<div class="code-block">
<pre>
// 定义模板
const template = document.createElement('template');
template.innerHTML = `
<style>
.product-card {
border: 1px solid #ddd;
border-radius: 10px;
overflow: hidden;
transition: transform 0.3s;
}
.product-card:hover {
transform: translateY(-5px);
}
.product-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.product-info {
padding: 15px;
}
.product-title {
font-size: 18px;
font-weight: bold;
margin: 0 0 10px 0;
color: #333;
}
.product-price {
font-size: 20px;
color: #667eea;
font-weight: bold;
}
.product-button {
width: 100%;
padding: 10px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
}
.product-button:hover {
background: #764ba2;
}
</style>
<div class="product-card">
<img class="product-image" src="" alt="">
<div class="product-info">
<h3 class="product-title"></h3>
<div class="product-price"></div>
<button class="product-button">加入购物车</button>
</div>
</div>
`;
class ProductCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
static get observedAttributes() {
return ['image', 'title', 'price'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'image') {
this.shadowRoot.querySelector('.product-image').src = newValue;
} else if (name === 'title') {
this.shadowRoot.querySelector('.product-title').textContent = newValue;
} else if (name === 'price') {
this.shadowRoot.querySelector('.product-price').textContent = `¥${newValue}`;
}
}
connectedCallback() {
this.shadowRoot.querySelector('.product-button').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('add-to-cart', {
detail: {
title: this.getAttribute('title'),
price: this.getAttribute('price')
},
bubbles: true
}));
});
}
}
customElements.define('product-card', ProductCard);
// 使用
/*
<product-card
image="product.jpg"
title="商品名称"
price="99.99">
</product-card>
*/
</pre>
</div>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 5px; margin-top: 20px;">
<strong>东巴文提示:</strong>HTML模板提供了高效的DOM复用机制。使用template元素定义模板,通过cloneNode(true)克隆模板内容。结合Web Components,能创建可复用、高性能的组件。
</div>
</div>
</body>
</html>
完成本章学习后,请尝试回答以下问题:
选择题: Web Components由哪三个主要技术组成?
填空题: Shadow DOM的mode参数可以是____或____。
简答题: Shadow DOM的作用是什么?它如何实现样式封装?
实践题: 创建一个自定义的倒计时组件,要求:
应用题: 比较Web Components与Vue/React组件的异同,分析各自的适用场景。