Shadow DOM提供了强大的封装能力,本章将深入探讨Shadow DOM的高级特性和最佳实践。
const shadowDOMStructure = {
document: {
description: '文档主树',
contains: ['light DOM elements']
},
shadowHost: {
description: 'Shadow DOM宿主元素',
contains: ['shadow root']
},
shadowRoot: {
description: 'Shadow DOM根节点',
contains: ['shadow tree']
},
shadowTree: {
description: '封装的DOM树',
contains: ['elements', 'styles', 'slots']
}
};
class EventTargetElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<div id="inner">
<button id="btn">点击</button>
</div>
`;
this.shadowRoot.getElementById('btn').addEventListener('click', (e) => {
console.log('Shadow内部 target:', e.target);
console.log('Shadow内部 composedPath:', e.composedPath());
});
}
}
document.addEventListener('click', (e) => {
console.log('外部 target:', e.target);
console.log('外部 composedPath:', e.composedPath());
});
class ThemedButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
--button-bg: var(--theme-primary, #007bff);
--button-color: var(--theme-on-primary, white);
--button-radius: var(--theme-radius, 4px);
}
button {
background: var(--button-bg);
color: var(--button-color);
border: none;
padding: 10px 20px;
border-radius: var(--button-radius);
cursor: pointer;
}
button:hover {
filter: brightness(0.9);
}
</style>
<button><slot></slot></button>
`;
}
}
customElements.define('themed-button', ThemedButton);
<style>
:root {
--theme-primary: #28a745;
--theme-on-primary: white;
--theme-radius: 8px;
}
</style>
<themed-button>绿色主题按钮</themed-button>
class PartElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.container { padding: 16px; }
.header { font-size: 18px; }
.content { margin-top: 8px; }
</style>
<div class="container">
<div class="header" part="header">
<slot name="header"></slot>
</div>
<div class="content" part="content">
<slot></slot>
</div>
</div>
`;
}
}
customElements.define('part-element', PartElement);
<style>
part-element::part(header) {
color: blue;
font-weight: bold;
}
part-element::part(content) {
background: #f5f5f5;
padding: 8px;
}
</style>
<part-element>
<span slot="header">标题</span>
<p>内容</p>
</part-element>
const themeSelector = {
description: '::theme选择器可以穿透所有Shadow DOM边界',
usage: `
:root {
--theme-color: blue;
}
/* 应用于所有匹配的元素 */
::theme(my-element) {
--custom-color: red;
}
`,
note: '::theme目前仍处于提案阶段,浏览器支持有限'
};
class OuterElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host { display: block; border: 1px solid blue; padding: 10px; }
</style>
<div>外层Shadow DOM</div>
<inner-element></inner-element>
`;
}
}
class InnerElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host { display: block; border: 1px solid red; padding: 10px; margin-top: 10px; }
</style>
<div>内层Shadow DOM</div>
`;
}
}
customElements.define('outer-element', OuterElement);
customElements.define('inner-element', InnerElement);
function deepQuerySelector(root, selector) {
const result = root.querySelector(selector);
if (result) return result;
const shadowHosts = root.querySelectorAll('*');
for (const host of shadowHosts) {
if (host.shadowRoot) {
const found = deepQuerySelector(host.shadowRoot, selector);
if (found) return found;
}
}
return null;
}
function deepQuerySelectorAll(root, selector, results = []) {
results.push(...root.querySelectorAll(selector));
const shadowHosts = root.querySelectorAll('*');
for (const host of shadowHosts) {
if (host.shadowRoot) {
deepQuerySelectorAll(host.shadowRoot, selector, results);
}
}
return results;
}
class FocusableElement extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'open',
delegatesFocus: true
});
this.shadowRoot.innerHTML = `
<style>
:host(:focus-within) {
outline: 2px solid blue;
}
input:focus {
outline: none;
border-color: blue;
}
</style>
<input type="text" placeholder="输入内容">
`;
}
}
customElements.define('focusable-element', FocusableElement);
class FocusGroup extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host { display: block; }
:host(:focus) { outline: 2px solid blue; }
.item:focus { background: #e0e0e0; }
</style>
<slot></slot>
`;
this.tabIndex = 0;
}
connectedCallback() {
this.addEventListener('keydown', this.handleKeydown.bind(this));
}
handleKeydown(e) {
const items = this.getFocusableItems();
const currentIndex = items.indexOf(document.activeElement);
switch (e.key) {
case 'ArrowDown':
case 'ArrowRight':
e.preventDefault();
const nextIndex = (currentIndex + 1) % items.length;
items[nextIndex].focus();
break;
case 'ArrowUp':
case 'ArrowLeft':
e.preventDefault();
const prevIndex = (currentIndex - 1 + items.length) % items.length;
items[prevIndex].focus();
break;
}
}
getFocusableItems() {
return Array.from(this.querySelectorAll('[tabindex="0"], button, a, input'));
}
}
class AccessibleButton extends HTMLElement {
static get observedAttributes() {
return ['disabled', 'aria-label', 'aria-pressed'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
button {
padding: 10px 20px;
border: none;
background: #007bff;
color: white;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button:focus {
outline: 2px solid #0056b3;
outline-offset: 2px;
}
</style>
<button id="btn" role="button">
<slot></slot>
</button>
`;
this._button = this.shadowRoot.getElementById('btn');
this._button.addEventListener('click', () => {
if (this.hasAttribute('aria-pressed')) {
const pressed = this.getAttribute('aria-pressed') === 'true';
this.setAttribute('aria-pressed', !pressed);
}
this.dispatchEvent(new CustomEvent('click', { bubbles: true }));
});
this._button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this._button.click();
}
});
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'disabled':
this._button.disabled = newValue !== null;
this._button.setAttribute('aria-disabled', newValue !== null);
break;
case 'aria-label':
this._button.setAttribute('aria-label', newValue || '');
break;
case 'aria-pressed':
this._button.setAttribute('aria-pressed', newValue || 'false');
break;
}
}
}
customElements.define('accessible-button', AccessibleButton);
class LiveRegion extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<div id="live"
role="status"
aria-live="polite"
aria-atomic="true">
</div>
`;
this._liveRegion = this.shadowRoot.getElementById('live');
}
announce(message, priority = 'polite') {
this._liveRegion.setAttribute('aria-live', priority);
this._liveRegion.textContent = '';
requestAnimationFrame(() => {
this._liveRegion.textContent = message;
});
}
}
customElements.define('live-region', LiveRegion);
const announcer = document.querySelector('live-region');
announcer.announce('操作成功完成');
announcer.announce('错误:请检查输入', 'assertive');
class LazyElement extends HTMLElement {
constructor() {
super();
this._rendered = false;
}
connectedCallback() {
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !this._rendered) {
this.render();
this._rendered = true;
observer.disconnect();
}
});
});
observer.observe(this);
} else {
this.render();
}
}
render() {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `...`;
}
}
class BatchUpdateElement extends HTMLElement {
constructor() {
super();
this._pendingUpdate = false;
this._pendingChanges = new Map();
this.attachShadow({ mode: 'open' });
}
requestUpdate(key, value) {
this._pendingChanges.set(key, value);
if (!this._pendingUpdate) {
this._pendingUpdate = true;
requestAnimationFrame(() => {
this.applyChanges();
this._pendingUpdate = false;
});
}
}
applyChanges() {
const changes = this._pendingChanges;
this._pendingChanges = new Map();
this.render(changes);
}
render(changes) {
// 应用所有变更
}
}
🎯 Shadow DOM最佳实践
- 样式隔离:利用CSS自定义属性实现主题定制
- 无障碍:正确设置ARIA属性和焦点管理
- 性能:使用模板缓存和延迟渲染
- 兼容性:提供优雅降级方案
🔍 调试技巧
- Chrome DevTools中勾选"Show user agent shadow DOM"
- 使用composedPath()追踪事件路径
- 检查shadowRoot.mode确认封装模式
下一章将探讨 [HTML Templates](file:///e:/db-w.cn/md_data/javascript/84_HTML Templates.md),学习HTML模板的使用方法。