15.4 Web Components 与 Shadow DOM
Web Components 基础
核心概念与技术栈
Web Components 是一套允许创建可重用、封装的自定义HTML元素的技术,包含三个主要技术:
- Custom Elements - 定义自定义元素及其行为
- Shadow DOM - 封装样式和标记结构
- HTML Templates - 定义可复用的标记模板
基本组件定义
class MyComponent extends HTMLElement {
constructor() {
super();
// 创建Shadow DOM
this.attachShadow({ mode: 'open' });
// 添加HTML内容
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ccc;
padding: 16px;
}
h3 {
color: var(--title-color, #333);
}
</style>
<h3><slot name="title">默认标题</slot></h3>
<div class="content">
<slot></slot>
</div>
`;
}
// 定义可观察属性
static get observedAttributes() {
return ['disabled', 'theme'];
}
// 属性变化回调
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'disabled') {
this.shadowRoot.querySelector('button').disabled = newValue !== null;
}
}
// 元素插入DOM时调用
connectedCallback() {
console.log('Custom element added to page');
}
// 元素从DOM移除时调用
disconnectedCallback() {
console.log('Custom element removed from page');
}
}
// 注册自定义元素
customElements.define('my-component', MyComponent);
Shadow DOM 深度解析
封装样式与标记
Shadow DOM 提供了样式和标记的封装,防止外部样式影响组件内部。
class StyledButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
button {
background: var(--button-bg, #6200ee);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<button><slot></slot></button>
`;
}
}
customElements.define('styled-button', StyledButton);
插槽(Slot)机制
插槽允许组件使用者自定义部分内容。
<!-- 使用组件 -->
<user-card>
<span slot="name">张三</span>
<span slot="email">[email protected]</span>
<div>其他信息...</div>
</user-card>
<!-- 组件定义 -->
<script>
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<div class="card">
<div class="header">
<slot name="name"></slot>
</div>
<div class="body">
<slot name="email"></slot>
<slot></slot>
</div>
</div>
`;
}
}
customElements.define('user-card', UserCard);
</script>
高级组件开发
响应式属性与数据绑定
class DataTable extends HTMLElement {
static get observedAttributes() {
return ['data'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._data = [];
}
get data() {
return this._data;
}
set data(value) {
this._data = value;
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'data') {
this.data = JSON.parse(newValue);
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
</style>
<table>
<thead>
<tr>
${Object.keys(this.data[0] || {}).map(key => `<th>${key}</th>`).join('')}
</tr>
</thead>
<tbody>
${this.data.map(row => `
<tr>
${Object.values(row).map(val => `<td>${val}</td>`).join('')}
</tr>
`).join('')}
</tbody>
</table>
`;
}
}
customElements.define('data-table', DataTable);
组件生命周期管理
class LifecycleDemo extends HTMLElement {
constructor() {
super();
console.log('1. Constructor called');
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `<p>生命周期演示</p>`;
}
connectedCallback() {
console.log('2. Component added to DOM');
this._interval = setInterval(() => {
this.dispatchEvent(new CustomEvent('timeupdate', {
detail: { time: new Date() }
}));
}, 1000);
}
disconnectedCallback() {
console.log('3. Component removed from DOM');
clearInterval(this._interval);
}
adoptedCallback() {
console.log('4. Component moved to new document');
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`5. Attribute ${name} changed from ${oldValue} to ${newValue}`);
}
}
customElements.define('lifecycle-demo', LifecycleDemo);
组件通信模式
事件通信
class CustomInput extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<input type="text">
`;
this.shadowRoot.querySelector('input').addEventListener('input', (e) => {
this.dispatchEvent(new CustomEvent('custom-change', {
detail: { value: e.target.value },
bubbles: true,
composed: true // 允许事件穿过Shadow DOM边界
}));
});
}
}
customElements.define('custom-input', CustomInput);
属性与属性反射
class ToggleSwitch extends HTMLElement {
static get observedAttributes() {
return ['checked'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(26px);
}
input {
display: none;
}
</style>
<label class="switch">
<input type="checkbox">
<span class="slider"></span>
</label>
`;
this._input = this.shadowRoot.querySelector('input');
this._input.addEventListener('change', () => {
this.checked = this._input.checked;
});
}
get checked() {
return this.hasAttribute('checked');
}
set checked(value) {
if (value) {
this.setAttribute('checked', '');
} else {
this.removeAttribute('checked');
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'checked') {
this._input.checked = this.checked;
}
}
}
customElements.define('toggle-switch', ToggleSwitch);
实战案例
可复用的模态对话框
class ModalDialog extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.dialog {
background: white;
border-radius: 8px;
padding: 20px;
min-width: 300px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
}
</style>
<div class="dialog">
<div class="header">
<h2><slot name="title">对话框标题</slot></h2>
<button class="close-btn">×</button>
</div>
<div class="content">
<slot></slot>
</div>
</div>
`;
this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => {
this.hide();
});
}
show() {
this.style.display = 'flex';
this.dispatchEvent(new Event('show'));
}
hide() {
this.style.display = 'none';
this.dispatchEvent(new Event('hide'));
}
}
customElements.define('modal-dialog', ModalDialog);
动态表单生成器
class FormBuilder extends HTMLElement {
static get observedAttributes() {
return ['schema'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._schema = [];
}
get schema() {
return this._schema;
}
set schema(value) {
this._schema = value;
this.renderForm();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'schema') {
this.schema = JSON.parse(newValue);
}
}
renderForm() {
this.shadowRoot.innerHTML = `
<style>
form {
display: grid;
gap: 16px;
max-width: 600px;
margin: 0 auto;
}
label {
display: block;
margin-bottom: 4px;
font-weight: bold;
}
input, select, textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
<form>
${this.schema.map(field => `
<div class="field">
<label for="${field.id}">${field.label}</label>
${this.renderField(field)}
</div>
`).join('')}
<button type="submit">提交</button>
</form>
`;
this.shadowRoot.querySelector('form').addEventListener('submit', e => {
e.preventDefault();
const formData = {};
this.schema.forEach(field => {
formData[field.name] = this.shadowRoot.getElementById(field.id).value;
});
this.dispatchEvent(new CustomEvent('form-submit', {
detail: formData
}));
});
}
renderField(field) {
switch (field.type) {
case 'select':
return `
<select id="${field.id}" name="${field.name}">
${field.options.map(opt => `
<option value="${opt.value}">${opt.label}</option>
`).join('')}
</select>
`;
case 'textarea':
return `<textarea id="${field.id}" name="${field.name}"></textarea>`;
default:
return `<input type="${field.type}" id="${field.id}" name="${field.name}">`;
}
}
}
customElements.define('form-builder', FormBuilder);
性能优化与最佳实践
使用模板元素
class TemplateDemo extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// 获取模板
const template = document.getElementById('template-demo');
const content = template.content.cloneNode(true);
this.shadowRoot.appendChild(content);
}
}
// HTML中的模板
/*
<template id="template-demo">
<style>
/* 样式定义 */
</style>
<div class="container">
<slot name="header"></slot>
<div class="content">
<slot></slot>
</div>
</div>
</template>
*/
customElements.define('template-demo', TemplateDemo);
延迟加载组件资源
class LazyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<div>加载中...</div>';
// 使用IntersectionObserver延迟加载
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
observer.unobserve(this);
this.loadResources();
}
});
observer.observe(this);
}
async loadResources() {
// 动态加载CSS
const style = document.createElement('link');
style.rel = 'stylesheet';
style.href = 'lazy-component.css';
this.shadowRoot.appendChild(style);
// 动态加载数据
const response = await fetch('data.json');
const data = await response.json();
this.renderContent(data);
}
renderContent(data) {
this.shadowRoot.innerHTML = `
<style>
/* 内联关键CSS */
</style>
<div class="component">
<!-- 渲染数据 -->
</div>
`;
}
}
customElements.define('lazy-component', LazyComponent);
现代Web组件生态
与框架集成
-
React中使用Web Components
function ReactComponent() { const [value, setValue] = useState(''); return ( <div> <custom-input onCustomChange={(e) => setValue(e.detail.value)} ></custom-input> <p>当前值: {value}</p> </div> ); } -
Vue中使用Web Components
<template> <div> <custom-input @custom-change="handleChange"></custom-input> <p>当前值: {{ value }}</p> </div> </template> <script> export default { data() { return { value: '' } }, methods: { handleChange(event) { this.value = event.detail.value; } } } </script>
开源工具与库
-
LitElement - 轻量级Web组件基类
import { LitElement, html, css } from 'lit-element'; class LitDemo extends LitElement { static get properties() { return { count: { type: Number } }; } static get styles() { return css` button { color: blue; } `; } constructor() { super(); this.count = 0; } render() { return html` <button @click=${() => this.count++}> 点击次数: ${this.count} </button> `; } } customElements.define('lit-demo', LitDemo); -
Stencil - 用于构建Web组件的编译器
-
FAST - Microsoft的Web组件框架
浏览器兼容性与Polyfill
对于不支持Web Components的旧浏览器,可以使用polyfill:
<!-- 加载Web Components polyfill -->
<script src="https://unpkg.com/@webcomponents/[email protected]/webcomponents-bundle.js"></script>
<!-- 使用type="module"和nomodule实现渐进增强 -->
<script type="module">
// 现代浏览器加载的代码
import './modern-component.js';
</script>
<script nomodule>
// 旧版浏览器加载的代码
const script = document.createElement('script');
script.src = './legacy-component.js';
document.head.appendChild(script);
</script>
#前端开发
分享于 2025-03-25