跟着 MDN 学 HTML day_49:深入理解 ShadowRoot 接口
📑 目录
| 左列章节 | 右列章节 |
|---|---|
| [一、创建 Shadow Root 并理解其模式](#左列章节 右列章节 一、创建 Shadow Root 并理解其模式 二、认识 ShadowRoot 的宿主元素 三、innerHTML 操作 Shadow DOM 四、焦点元素与选区 五、坐标定位方法 六、delegatesFocus 代理焦点) | [二、认识 ShadowRoot 的宿主元素](#左列章节 右列章节 一、创建 Shadow Root 并理解其模式 二、认识 ShadowRoot 的宿主元素 三、innerHTML 操作 Shadow DOM 四、焦点元素与选区 五、坐标定位方法 六、delegatesFocus 代理焦点) |
| [三、innerHTML 操作 Shadow DOM](#左列章节 右列章节 一、创建 Shadow Root 并理解其模式 二、认识 ShadowRoot 的宿主元素 三、innerHTML 操作 Shadow DOM 四、焦点元素与选区 五、坐标定位方法 六、delegatesFocus 代理焦点) | [四、焦点元素与选区](#左列章节 右列章节 一、创建 Shadow Root 并理解其模式 二、认识 ShadowRoot 的宿主元素 三、innerHTML 操作 Shadow DOM 四、焦点元素与选区 五、坐标定位方法 六、delegatesFocus 代理焦点) |
| [五、坐标定位方法](#左列章节 右列章节 一、创建 Shadow Root 并理解其模式 二、认识 ShadowRoot 的宿主元素 三、innerHTML 操作 Shadow DOM 四、焦点元素与选区 五、坐标定位方法 六、delegatesFocus 代理焦点) | [六、delegatesFocus 代理焦点](#左列章节 右列章节 一、创建 Shadow Root 并理解其模式 二、认识 ShadowRoot 的宿主元素 三、innerHTML 操作 Shadow DOM 四、焦点元素与选区 五、坐标定位方法 六、delegatesFocus 代理焦点) |
一、创建 Shadow Root 并理解其模式
ShadowRoot 不能通过构造函数直接创建,它必须通过 Element 的 attachShadow 方法来生成。在创建时,需要指定 mode 参数,它决定了 Shadow DOM 的内部实现是否对外部 JavaScript 开放。
代码示例:open 与 closed 两种模式
javascript
// 获取宿主元素
const openHost = document.getElementById('openComponent');
const closedHost = document.getElementById('closedComponent');
// 创建开放模式的 Shadow Root
const openShadow = openHost.attachShadow({ mode: 'open' });
openShadow.innerHTML = `
<style>
p {
color: blue;
font-weight: bold;
padding: 10px;
border: 1px solid blue;
}
</style>
<p>这是 Shadow DOM 中的内容,模式为 open</p>
`;
// 创建封闭模式的 Shadow Root
const closedShadow = closedHost.attachShadow({ mode: 'closed' });
closedShadow.innerHTML = `
<style>
p {
color: red;
font-weight: bold;
padding: 10px;
border: 1px solid red;
}
</style>
<p>这是 Shadow DOM 中的内容,模式为 closed</p>
`;
代码示例:两种模式的可访问性差异
javascript
// 通过 host 元素的 shadowRoot 属性访问
console.log('开放模式的 shadowRoot:', openHost.shadowRoot); // ShadowRoot 对象
console.log('开放模式的 mode:', openHost.shadowRoot.mode); // "open"
// 封闭模式下 host.shadowRoot 返回 null
console.log('封闭模式的 shadowRoot:', closedHost.shadowRoot); // null
// 如果在创建时保存了引用,仍然可以操作封闭模式的 ShadowRoot
console.log('封闭模式的 mode(通过保存的引用访问):', closedShadow.mode); // "closed"
核心结论 :
mode属性有两种可选值:open和closed。当mode为open时,可以通过宿主元素的shadowRoot属性获取到ShadowRoot实例,允许外部 JavaScript 访问和修改内部结构。当mode为closed时,host.shadowRoot返回null,外部脚本无法直接访问内部实现。
⚠️ 【重点 / 面试考点 / 易错点】
| 模式 | 外部可访问性 | host.shadowRoot |
适用场景 |
|---|---|---|---|
open |
✅ 外部可访问 | 返回 ShadowRoot 实例 |
需要外部脚本交互的组件 |
closed |
❌ 外部不可访问 | 返回 null |
封装要求严格的组件 |
closed并非绝对安全 ------如果创建时保存了ShadowRoot引用,仍可通过该引用操作closed的主要作用是防止外部意外访问,而非提供安全隔离- 大多数场景推荐使用
open模式,便于调试和外部样式穿透
二、认识 ShadowRoot 的宿主元素
每一个 ShadowRoot 都会附加到一个宿主元素 上,通过 host 属性可以获取到这个宿主 DOM 元素的引用。这一属性在编写可复用的自定义组件时非常重要。
代码示例:通过 host 属性访问宿主元素
javascript
const container = document.getElementById('cardContainer');
const shadowRoot = container.attachShadow({ mode: 'open' });
// 在 Shadow DOM 中构建卡片结构
shadowRoot.innerHTML = `
<style>
.card {
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card h3 { margin-top: 0; color: #333; }
.card p { color: #666; line-height: 1.6; }
button {
background-color: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
</style>
<div class="card">
<h3>卡片标题</h3>
<p>这是卡片内部的描述文字,样式不受外部 CSS 影响。</p>
<button id="actionBtn">点击我</button>
</div>
`;
// 使用 host 属性从 Shadow DOM 内部获取宿主元素
const hostElement = shadowRoot.host;
console.log('ShadowRoot 的宿主元素:', hostElement);
console.log('宿主元素的 ID:', hostElement.id);
console.log('宿主元素是否与容器相同:', hostElement === container); // true
代码示例:通过 host 与外部交互
javascript
// 在 Shadow DOM 中给按钮添加事件监听
const button = shadowRoot.querySelector('#actionBtn');
button.addEventListener('click', function() {
// 通过 host 属性获取宿主元素,进而修改其属性
shadowRoot.host.setAttribute('data-clicked', 'true');
shadowRoot.host.style.border = '2px solid #007bff';
// 在 Shadow DOM 内部显示响应
const messageDiv = document.createElement('p');
messageDiv.textContent = '宿主元素的 data-clicked 已设置为: ' +
shadowRoot.host.getAttribute('data-clicked');
messageDiv.style.color = 'green';
shadowRoot.querySelector('.card').appendChild(messageDiv);
});
核心结论 :
host属性建立了 Shadow DOM 内部与外部宿主元素之间的桥梁。通过host属性,组件内部可以读取宿主的属性、数据属性,甚至修改宿主的样式类来反映组件状态。host属性是只读的,无法修改。
三、使用 innerHTML 操作 Shadow DOM 内容
ShadowRoot 接口提供了 innerHTML 属性,允许开发者以字符串形式读写 Shadow DOM 内部的 HTML 结构。这与普通 DOM 元素上的 innerHTML 用法完全一致。
代码示例:使用 innerHTML 设置初始内容
javascript
const host = document.getElementById('dynamicComponent');
const shadowRoot = host.attachShadow({ mode: 'open' });
// 使用 innerHTML 设置初始内容
shadowRoot.innerHTML = `
<style>
.content {
font-family: 'Arial', sans-serif;
padding: 16px;
background-color: #e9ecef;
}
.item {
background-color: white;
margin: 8px 0;
padding: 12px;
border-radius: 4px;
border-left: 4px solid #28a745;
}
</style>
<div class="content">
<h4>动态内容区域</h4>
<div class="item">项目一:初始内容</div>
<div class="item">项目二:初始内容</div>
</div>
`;
代码示例:读取和更新 innerHTML
javascript
// 读取 Shadow DOM 的 HTML 结构
console.log('当前 Shadow DOM 结构:', shadowRoot.innerHTML);
// 替换整个 Shadow DOM 内容(完全重写)
shadowRoot.innerHTML = `
<style>
.content {
padding: 16px;
background-color: #d4edda;
}
.item {
background-color: white;
margin: 8px 0;
padding: 12px;
border-left: 4px solid #dc3545;
}
</style>
<div class="content">
<h4>内容已完全更新</h4>
<div class="item">全新内容 A</div>
<div class="item">全新内容 B</div>
<div class="item">全新内容 C</div>
</div>
`;
代码示例:追加内容(避免全部重写)
javascript
// 获取已有容器并追加内容,比重写 innerHTML 性能更好
const contentDiv = shadowRoot.querySelector('.content');
if (contentDiv) {
const newItem = document.createElement('div');
newItem.className = 'item highlight';
newItem.textContent = '新追加的项目 ' + new Date().toLocaleTimeString();
contentDiv.appendChild(newItem);
}
核心结论 :设置
innerHTML会完全替换 Shadow DOM 中的所有内容。如果只需要追加内容,应使用appendChild或insertAdjacentHTML等方法。频繁使用innerHTML进行大量内容替换可能影响性能,处理复杂组件时应考虑更细粒度的 DOM 操作方法。
⚠️ 【重点 / 面试考点】
innerHTML设置时会完全替换 Shadow DOM 内所有内容,包括样式和结构- 频繁重写
innerHTML会导致性能下降 和事件监听器丢失 - 追加内容优先使用
appendChild/insertAdjacentHTML,避免不必要的全部重写 - Shadow DOM 内的
<style>标签仅作用于 Shadow DOM 内部,不会影响外部页面 - 这是 Shadow DOM 样式封装的核心机制
四、获取 Shadow DOM 中的焦点元素与选区
ShadowRoot 接口通过 DocumentOrShadowRoot 混入,获得了 activeElement 属性和 getSelection 方法,使得可以在 Shadow DOM 的上下文中追踪焦点 和文本选择状态。
代码示例:activeElement 追踪焦点元素
javascript
const formHost = document.getElementById('formComponent');
const shadow = formHost.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.form-group { margin-bottom: 16px; padding: 20px; background: #f5f5f5; }
input, textarea {
width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;
}
input:focus, textarea:focus {
outline: none; border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
</style>
<div class="form-group">
<label for="nameInput">姓名:</label>
<input type="text" id="nameInput" placeholder="请输入姓名">
</div>
<div class="form-group">
<label for="emailInput">邮箱:</label>
<input type="email" id="emailInput" placeholder="请输入邮箱">
</div>
`;
// 检查 Shadow DOM 内的焦点元素
const activeElement = shadow.activeElement;
if (activeElement) {
console.log('Shadow DOM 中当前焦点元素:');
console.log(' 标签名:', activeElement.tagName);
console.log(' ID:', activeElement.id);
console.log(' 值:', activeElement.value);
}
代码示例:getSelection 获取文本选区
javascript
// 获取用户在 Shadow DOM 中的文本选择状态
const selection = shadow.getSelection();
if (selection && selection.toString().length > 0) {
console.log('选中的文本:', selection.toString());
console.log('选区起始节点:', selection.anchorNode);
console.log('选区起始偏移量:', selection.anchorOffset);
console.log('选区结束节点:', selection.focusNode);
console.log('选区结束偏移量:', selection.focusOffset);
} else {
console.log('当前没有选中的文本');
}
核心结论 :
shadow.activeElement返回 Shadow DOM 树中当前拥有焦点的元素(焦点不在 Shadow DOM 内部时返回null)。document.activeElement在 Shadow DOM 场景下返回的是宿主元素 ,而shadow.activeElement能够穿透封装获取内部具体的焦点元素。
五、使用坐标定位方法查找元素
ShadowRoot 接口提供了 elementFromPoint 和 elementsFromPoint 方法,允许开发者根据视口坐标在 Shadow DOM 内部查找对应的元素。这些方法在处理鼠标交互和拖拽功能时非常有用。
代码示例:elementFromPoint 与 elementsFromPoint
javascript
const host = document.getElementById('interactiveHost');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.container {
width: 100%; height: 100%; position: relative;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.box {
position: absolute; width: 80px; height: 80px;
border-radius: 8px; display: flex;
align-items: center; justify-content: center;
color: white; font-weight: bold; font-size: 12px;
}
.box-a { top: 30px; left: 30px; background: rgba(255,255,255,0.3); }
.box-b { top: 30px; right: 30px; background: rgba(255,255,255,0.4); }
</style>
<div class="container">
<div class="box box-a" data-name="A区">区域 A</div>
<div class="box box-b" data-name="B区">区域 B</div>
</div>
`;
// 监听宿主元素上的鼠标移动事件
host.addEventListener('mousemove', function(event) {
const rect = host.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// 获取指定坐标的最顶层元素
const topElement = shadow.elementFromPoint(x, y);
// 获取指定坐标的所有元素(从顶到底)
const allElements = shadow.elementsFromPoint(x, y);
console.log('坐标: (' + x + ', ' + y + ')');
console.log('最顶层元素:', topElement?.getAttribute('data-name') || '无');
console.log('该坐标下的元素层级:');
allElements.forEach(function(el, index) {
console.log(' ' + (index + 1) + '. ' + (el.getAttribute('data-name') || el.tagName));
});
});
代码示例:点击拾取元素
javascript
host.addEventListener('click', function(event) {
const rect = host.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const clickedElement = shadow.elementFromPoint(x, y);
if (clickedElement) {
const name = clickedElement.getAttribute('data-name');
if (name) {
console.log('点击了 Shadow DOM 中的:', name);
}
}
});
核心结论 :
elementFromPoint返回指定坐标点处最顶层 的元素,elementsFromPoint返回该坐标点处从顶到底的所有元素数组 。坐标是相对于视口的,计算 Shadow DOM 内部坐标时需减去宿主元素的偏移量 。document.elementFromPoint返回宿主元素 ,而shadow.elementFromPoint能穿透封装返回实际内部元素。
六、代理焦点行为与 delegatesFocus 属性
ShadowRoot 接口的 delegatesFocus 属性是一个只读布尔值 ,在 Shadow Root 创建时通过配置项设定。当该属性为 true 时,Shadow DOM 内部的焦点行为会以特殊方式代理到宿主元素上。
代码示例:创建带 delegatesFocus 的 Shadow Root
javascript
// 创建启用了 delegatesFocus 的 Shadow Root
const delegatingHost = document.getElementById('delegatingHost');
const delegatingShadow = delegatingHost.attachShadow({
mode: 'open',
delegatesFocus: true // 启用焦点委托
});
delegatingShadow.innerHTML = `
<style>
input {
padding: 10px; border: 2px solid #ccc;
border-radius: 4px; width: 200px; font-size: 14px;
}
input:focus { border-color: #007bff; outline: none; }
</style>
<input type="text" placeholder="启用了焦点委托的输入框">
<button>内部按钮</button>
`;
// 创建未启用 delegatesFocus 的 Shadow Root(对比)
const normalHost = document.getElementById('normalHost');
const normalShadow = normalHost.attachShadow({
mode: 'open',
delegatesFocus: false // 不启用焦点委托(默认)
});
normalShadow.innerHTML = `
<style>
input { padding: 10px; border: 2px solid #ccc; border-radius: 4px; width: 200px; }
input:focus { border-color: #007bff; outline: none; }
</style>
<input type="text" placeholder="未启用焦点委托的输入框">
`;
代码示例:delegatesFocus 的行为差异
javascript
// 检查 delegatesFocus 属性
console.log('第一个 ShadowRoot 的 delegatesFocus:', delegatingShadow.delegatesFocus); // true
console.log('第二个 ShadowRoot 的 delegatesFocus:', normalShadow.delegatesFocus); // false
// 当 delegatesFocus 为 true 时:
// - Shadow DOM 内部元素获得焦点时,document.activeElement 指向宿主元素
// - 宿主元素匹配 :focus 和 :focus-within CSS 伪类
// - 外部页面可感知组件内部的焦点状态
// 监听焦点事件
delegatingHost.addEventListener('focus', function() {
console.log('delegatingHost 获得焦点(来自内部元素的委托)');
console.log('document.activeElement:', document.activeElement); // delegatingHost
console.log('Shadow 内 activeElement:', delegatingShadow.activeElement); // input 元素
});
核心结论 :
delegatesFocus: true解决了 Shadow DOM 组件的焦点感知问题------当内部可聚焦元素获得焦点时,宿主元素在文档层面表现为获得焦点状态。这使得基于 Shadow DOM 的组件能够更好地与页面的无障碍功能 和:focus/:focus-withinCSS 伪类集成。
⚠️ 【重点 / 面试考点】
| 属性/方法 | 功能 | 关键特性 |
|---|---|---|
mode |
访问控制 | open 可外部访问,closed 不可访问 |
host |
获取宿主元素 | 只读,建立内外桥梁 |
innerHTML |
读写 HTML 结构 | 完全替换,注意性能 |
activeElement |
焦点元素 | 穿透封装获取内部焦点 |
getSelection() |
文本选区 | 获取 Shadow DOM 内选区 |
elementFromPoint |
坐标定位 | 单元素,坐标相对视口 |
elementsFromPoint |
坐标定位 | 元素数组,从顶到底 |
delegatesFocus |
焦点代理 | 内部焦点委托给宿主 |
delegatesFocus必须在attachShadow()时设置,创建后不可修改document.activeElement遇到 Shadow DOM 只返回宿主元素 ,要获取内部焦点用shadow.activeElementclosed模式不能阻挡开发者工具检查,仅阻止 JavaScript 访问
七、ShadowRoot 核心方法与属性速查
Shadow DOM 架构示意图
host 属性
shadowRoot 属性
Light DOM
普通页面
Host Element
宿主元素
Shadow Root
影子根
Shadow DOM
内部 DOM 树
内部样式 style
内部元素 slot/content
内部脚本/事件
核心属性
| 属性 | 类型 | 说明 |
|---|---|---|
mode |
string |
"open" 或 "closed",创建时设定 |
host |
Element |
宿主元素引用,只读 |
innerHTML |
string |
Shadow DOM 内部的 HTML 字符串 |
delegatesFocus |
boolean |
是否启用焦点代理,创建时设定 |
activeElement |
Element / null |
当前获得焦点的内部元素 |
核心方法
| 方法 | 返回值 | 说明 |
|---|---|---|
getSelection() |
Selection / null |
获取文本选区 |
elementFromPoint(x, y) |
Element / null |
坐标点最顶层元素 |
elementsFromPoint(x, y) |
Element[] |
坐标点所有元素 |
创建方式
javascript
// 标准创建方式
const shadow = element.attachShadow({
mode: 'open', // 或 'closed'
delegatesFocus: true // 可选,默认 false
});
✅ 文档总结
ShadowRoot通过element.attachShadow({ mode, delegatesFocus })创建,不能通过构造函数直接实例化mode: 'open'允许外部通过host.shadowRoot访问内部;mode: 'closed'返回null,但保存的引用仍可操作host属性只读,是 Shadow DOM 内部与外部宿主元素的唯一桥梁innerHTML可读写 Shadow DOM 结构,但完全替换性能较差,追加内容优先用appendChild- Shadow DOM 内
<style>仅作用于内部,是样式封装的核心机制 activeElement获取内部焦点元素;getSelection()获取内部文本选区elementFromPoint/elementsFromPoint实现坐标定位,坐标相对视口需减去宿主偏移delegatesFocus: true将内部焦点代理给宿主,使:focus/:focus-within伪类正常工作
完整实践示例
javascript
// 综合应用:封装一个带焦点代理和动态内容的 Shadow DOM 组件
class ShadowCard extends HTMLElement {
constructor() {
super();
// 创建 Shadow Root,启用焦点代理
this.shadow = this.attachShadow({
mode: 'open',
delegatesFocus: true
});
// 初始化结构
this.shadow.innerHTML = `
<style>
:host {
display: block;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
background: white;
transition: border-color 0.3s, box-shadow 0.3s;
}
:host(:focus-within) {
border-color: #007bff;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15);
}
.card-header {
font-size: 18px;
font-weight: bold;
margin-bottom: 12px;
color: #333;
}
.card-body {
color: #666;
line-height: 1.6;
margin-bottom: 16px;
}
input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 6px;
box-sizing: border-box;
font-size: 14px;
}
input:focus {
border-color: #007bff;
outline: none;
}
.status {
margin-top: 12px;
font-size: 12px;
color: #999;
}
</style>
<div class="card-header">Shadow Card 组件</div>
<div class="card-body">
<slot>默认内容</slot>
</div>
<input type="text" placeholder="在此输入...">
<div class="status">等待输入...</div>
`;
// 绑定内部事件
this.bindEvents();
}
bindEvents() {
const input = this.shadow.querySelector('input');
const status = this.shadow.querySelector('.status');
// 监听输入事件
input.addEventListener('input', () => {
status.textContent = '当前输入: ' + input.value;
status.style.color = '#28a745';
});
// 监听焦点事件
input.addEventListener('focus', () => {
console.log('输入框获得焦点');
console.log('shadow.activeElement:', this.shadow.activeElement?.tagName);
console.log('document.activeElement:', document.activeElement?.tagName); // shadow-card
});
// 监听选区变化
input.addEventListener('select', () => {
const selection = this.shadow.getSelection();
console.log('选中文本:', selection?.toString());
});
}
// 公共 API:动态更新内容
updateContent(html) {
const body = this.shadow.querySelector('.card-body');
body.innerHTML = html;
}
// 公共 API:获取当前输入值
getValue() {
return this.shadow.querySelector('input').value;
}
}
// 注册自定义元素
customElements.define('shadow-card', ShadowCard);
// 使用示例
document.addEventListener('DOMContentLoaded', () => {
const card = document.querySelector('shadow-card');
// 通过公共 API 更新内容
card.updateContent('<p>这是动态更新的内容!</p>');
// delegatesFocus 使得宿主元素能感知内部焦点
card.addEventListener('focus', () => {
console.log('卡片组件获得焦点(由内部 input 委托)');
});
});
通过动手实践,可以更直观地体会到 ShadowRoot 接口如何为 Web 组件提供真正的封装能力,以及 delegatesFocus、activeElement 等特性在构建无障碍、可交互组件时的重要作用。
想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!