Shadow DOM深入

Shadow DOM提供了强大的封装能力,本章将深入探讨Shadow DOM的高级特性和最佳实践。

Shadow DOM结构

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());
});

样式穿透

CSS自定义属性

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>

::part选择器

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>

::theme选择器

const themeSelector = {
    description: '::theme选择器可以穿透所有Shadow DOM边界',
    
    usage: `
        :root {
            --theme-color: blue;
        }
        
        /* 应用于所有匹配的元素 */
        ::theme(my-element) {
            --custom-color: red;
        }
    `,
    
    note: '::theme目前仍处于提案阶段,浏览器支持有限'
};

多层Shadow DOM

嵌套Shadow DOM

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;
}

焦点管理

delegatesFocus

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'));
    }
}

ARIA和无障碍

角色和属性

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最佳实践

  1. 样式隔离:利用CSS自定义属性实现主题定制
  2. 无障碍:正确设置ARIA属性和焦点管理
  3. 性能:使用模板缓存和延迟渲染
  4. 兼容性:提供优雅降级方案

🔍 调试技巧

  • 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模板的使用方法。