主题
Shadow DOM 穿透
本文引用的源码文件
本文引用的文件
目录
- 引言
- 项目结构中的 Shadow DOM 能力定位
- 核心组件与职责划分
- 架构总览:从工具调用到 Shadow Root 遍历
- 穿透算法详解
- 跨域 iframe 与安全边界
- Web Components 内部内容访问示例
- 性能、缓存与内存泄漏防护
- 与现代前端框架的集成实践
- 故障排查指南
- 结论
引言
Shadow DOM 是 Web Components 的核心隔离机制之一。它把组件的内部结构、样式和事件处理封装在一个独立的文档片段中,宿主页面无法直接通过常规 document.querySelector 访问其内部节点。对浏览器扩展、自动化脚本、网页分析工具和智能体交互系统而言,这意味着必须实现“穿透”能力:递归进入 shadowRoot,同时兼容同源 iframe,并在权限受限或跨域场景下安全降级。
本仓库在扩展端 Content Script 层实现了完整的 Shadow DOM 穿透工具集,并在此基础上提供元素查询、文本提取、HTML 快照、选择范围获取、可见性判断、滚动收集以及基于 ref 的元素操作等上层能力。这些能力被多个工具模块复用,支撑了“精准操作任意前端框架页面”的目标。
项目结构中的 Shadow DOM 能力定位
Shadow DOM 穿透能力集中在一个专用工具模块中,其他功能模块通过导入方式复用该能力,而不是各自重复实现。
graph TB shadow_utils["shadow-dom-utils.js<br/>Shadow DOM 与 iframe 深度工具"] --> page_interaction["page-interaction.js<br/>元素快照、ref 解析、交互树"] shadow_utils --> interaction_tools["interaction-tools.js<br/>点击、填充、键盘、滚动、下拉"] shadow_utils --> page_tools["page-tools.js<br/>页面文本、标题、链接、HTML 导出"] shadow_utils --> advanced_tools["advanced-tools.js<br/>高级工具使用 deepGetText"] shadow_utils --> selection_toolbar["selection-toolbar.js<br/>选择区域工具栏"]
图表来源
- src/content/shadow-dom-utils.js#L1-L443
- src/content/page-interaction.js#L1-L10
- src/content/interaction-tools.js#L1-L6
- src/content/page-tools.js#L59-L108
- src/content/advanced-tools.js#L4
- src/content/selection-toolbar.js#L3
章节来源
- src/content/shadow-dom-utils.js#L1-L443
- src/content/page-interaction.js#L1-L10
- src/content/interaction-tools.js#L1-L6
- src/content/page-tools.js#L59-L108
核心组件与职责划分
Shadow DOM 穿透相关代码按“底层遍历工具 + 上层交互工具”分层组织。
| 模块 | 主要职责 | 关键函数或行为 |
|---|---|---|
shadow-dom-utils.js | 提供递归穿透 Shadow DOM 和同源 iframe 的基础能力 | deepQuerySelector、deepQuerySelectorAll、deepGetText、deepGetHtml、getDeepSelector、findShadowRoot、attachSelectionListeners |
page-interaction.js | 构建可交互元素快照、分配 ref、解析 ref、渲染交互树 | queryInteractiveElements、collectSnapshotOps、resolveByRef、interactByRef |
interaction-tools.js | 面向模型或工具的原子操作:点击、填充表单、键盘输入、滚动、等待状态 | clickElement、fillForm、keyboardInput、scrollToPosition、waitForElement、clickByText |
page-tools.js | 页面级信息导出:标题、URL、正文、标题列表、链接、完整 HTML | getPageInfo、getFullHtml |
advanced-tools.js | 高级工具入口,复用 deepGetText 做内容读取 | 使用 deepGetText |
selection-toolbar.js | 选择区域工具栏,依赖 Shadow DOM 内的选择变化 | deepGetSelection、getRangeViewportPosition、attachSelectionListeners |
章节来源
- src/content/shadow-dom-utils.js#L12-L86
- src/content/page-interaction.js#L51-L152
- src/content/interaction-tools.js#L100-L148
- src/content/page-tools.js#L59-L108
- src/content/selection-toolbar.js#L3
架构总览:从工具调用到 Shadow Root 遍历
以“点击按钮”为例,调用链会经过工具层、选择器穿透层、DOM 操作层,并最终触发目标元素的交互逻辑。
sequenceDiagram participant Caller as "调用方" participant InteractionTools as "interaction-tools.js" participant PageInteraction as "page-interaction.js" participant ShadowUtils as "shadow-dom-utils.js" participant Target as "目标元素" Caller->>InteractionTools : "clickElement(selector)" InteractionTools->>ShadowUtils : "deepQuerySelector(selector)" ShadowUtils-->>InteractionTools : "返回元素或 null" InteractionTools->>Target : "element.click()" InteractionTools->>InteractionTools : "autoWaitAfterAction(检测导航/DOM 变化)" InteractionTools-->>Caller : "成功结果 + 等待信息"
图表来源
另一个典型流程是“按文本点击”,它会先通过 deepQuerySelectorAll 遍历所有候选元素,再匹配文本、检查可见性并执行点击。
sequenceDiagram participant Caller as "调用方" participant InteractionTools as "interaction-tools.js" participant ShadowUtils as "shadow-dom-utils.js" participant Target as "目标元素" Caller->>InteractionTools : "clickByText(text, options)" InteractionTools->>ShadowUtils : "deepQuerySelectorAll(多个选择器)" ShadowUtils-->>InteractionTools : "候选元素集合" InteractionTools->>InteractionTools : "匹配文本 + 可见性检查" InteractionTools->>Target : "dispatchEvent / click()" InteractionTools->>InteractionTools : "autoWaitAfterAction" InteractionTools-->>Caller : "命中结果或诊断错误"
图表来源
穿透算法详解
递归遍历 Shadow Root
deepQuerySelector 和 deepQuerySelectorAll 是穿透能力的核心。它们的工作方式是:
- 如果当前根节点支持
querySelectorAll,则遍历所有子元素。 - 对每个有
shadowRoot的元素,递归进入 Shadow Root 继续搜索。 - 对
IFRAME或FRAME元素,尝试读取contentDocument或contentWindow.document,并递归进入同源文档。 - 在 Shadow DOM 和 iframe 优先搜索后,再在当前根节点上执行一次选择器匹配。
- 最大递归深度默认是 5,防止无限嵌套导致性能问题。
flowchart TD
Start(["开始"]) --> CheckDepth["检查当前深度是否超过最大深度"]
CheckDepth --> |超过| ReturnNull["返回空结果"]
CheckDepth --> |未超过| QueryAll["遍历当前根的所有元素"]
QueryAll --> HasShadow{"元素是否有 shadowRoot"}
HasShadow --> |是| RecurseShadow["递归进入 shadowRoot 搜索"]
RecurseShadow --> FoundShadow{"Shadow 中找到目标"}
FoundShadow --> |是| ReturnFound["返回找到的元素"]
FoundShadow --> |否| NextEl["继续下一个元素"]
HasShadow --> |否| IsIframe{"是否为 IFRAME 或 FRAME"}
IsIframe --> |是| TryIframe["尝试读取 contentDocument"]
TryIframe --> CanAccess{"能否访问文档"}
CanAccess --> |能| RecurseIframe["递归进入 iframe 文档"]
CanAccess --> |不能| SkipIframe["跳过跨域 iframe"]
IsIframe --> |否| NextEl
NextEl --> Continue["继续遍历"]
Continue --> AfterTraversal["遍历完成后在当前根执行选择器"]
AfterTraversal --> HasResult{"当前根找到结果"}
HasResult --> |是| ReturnCurrent["返回当前根结果"]
HasResult --> |否| End(["结束"])
图表来源
样式隔离处理
Shadow DOM 的样式隔离意味着宿主页面的 CSS 通常不会直接影响组件内部样式,反之亦然。本项目的穿透能力并不直接修改 Shadow DOM 内部样式,而是通过以下方式间接适配样式隔离:
- 使用
innerText、textContent、innerHTML、outerHTML读取文本和 HTML。 - 使用
getBoundingClientRect、checkVisibility、getComputedStyle判断可见性和布局。 - 使用
getRootNode判断元素是否位于 Shadow Root 内。 - 生成包含 Shadow Host 路径的选择器时,使用自定义前缀标记 Shadow DOM 层级。
这种方式避免了直接注入样式破坏组件内部样式隔离,同时保证外部工具能正确识别元素位置和可见性。
章节来源
- src/content/shadow-dom-utils.js#L135-L173
- src/content/shadow-dom-utils.js#L183-L232
- src/content/shadow-dom-utils.js#L239-L284
- src/content/page-interaction.js#L205-L223
事件冒泡机制
Shadow DOM 的事件模型允许某些事件冒泡到宿主文档,但事件目标仍保持在 Shadow Tree 内部。本项目在以下场景中考虑了事件冒泡:
- 点击、悬停、键盘输入等操作都会显式派发
bubbles: true的事件,以便 React、Vue、Angular 等框架的合成事件系统能够捕获。 - 选择变化监听器通过
attachSelectionListeners递归挂载到 Shadow Root,确保用户在选择 Shadow DOM 内部文本时也能触发工具栏更新。 - 表单填充时使用原生 setter 设置值,并派发
input、change事件,绕过受控组件的状态隔离。
sequenceDiagram participant User as "用户" participant ShadowHost as "Shadow Host" participant ShadowRoot as "Shadow Root" participant InternalEl as "内部元素" participant ContentScript as "Content Script" User->>InternalEl : "点击内部元素" InternalEl->>ShadowRoot : "触发内部事件" ShadowRoot->>ShadowHost : "冒泡到宿主" ShadowHost->>ContentScript : "Content Script 监听或派发合成事件" ContentScript->>InternalEl : "dispatchEvent(input/change/click)"
图表来源
章节来源
跨域 iframe 与安全边界
Shadow DOM 本身不是跨域问题,但项目中同时处理了 iframe 和 Shadow DOM。对于 iframe:
- 如果
contentDocument或contentWindow.document可访问,说明是同域或扩展权限允许访问,工具会继续递归遍历。 - 如果访问抛出异常,代码会捕获并跳过该 iframe,避免中断整个遍历过程。
- 这种设计符合浏览器的同源策略:跨域文档不可直接读写,只能作为黑盒存在。
对于 Shadow DOM:
- 只要当前 Content Script 能访问宿主元素,就能读取其
shadowRoot。 - 如果宿主元素本身不可访问,则无法进入 Shadow Tree。
- 工具通过 try-catch 包裹可能失败的 DOM 访问,保证局部失败不影响整体遍历。
flowchart TD
Start(["开始遍历"]) --> CheckIframe{"是否遇到 iframe"}
CheckIframe --> |否| CheckShadow{"是否遇到 shadowRoot"}
CheckShadow --> |是| EnterShadow["递归进入 Shadow Root"]
CheckShadow --> |否| Continue["继续遍历"]
CheckIframe --> |是| TryAccess["尝试访问 contentDocument"]
TryAccess --> AccessOK{"访问成功"}
AccessOK --> |是| EnterIframe["递归进入 iframe 文档"]
AccessOK --> |否| SkipIframe["记录跨域限制并跳过"]
EnterShadow --> Continue
EnterIframe --> Continue
SkipIframe --> Continue
Continue --> End(["结束"])
图表来源
章节来源
Web Components 内部内容访问示例
本节给出在实际使用中如何操作组件内部元素、读取组件状态和触发组件方法。为避免泄露具体实现细节,这里以“步骤 + 对应源码位置”的方式说明。
示例一:点击 Shadow DOM 内部的按钮
适用场景:页面外层有一个普通按钮,但真正可用的按钮在某个 Web Component 的 Shadow Root 中。
推荐步骤:
- 使用
deepQuerySelector传入目标选择器。 - 如果返回元素为空,说明选择器无法穿透 Shadow DOM。
- 调整选择器或使用
query_elements生成的 ref。 - 调用
clickElement或interactByRef执行点击。
参考路径:
- src/content/shadow-dom-utils.js#L12-L45
- src/content/interaction-tools.js#L106-L148
- src/content/page-interaction.js#L793-L800
示例二:读取 Shadow DOM 内部文本
适用场景:需要判断组件是否显示“订单已确认”之类的状态文本。
推荐步骤:
- 使用
deepGetText获取包含 Shadow DOM 和同源 iframe 的合并文本。 - 在业务逻辑中判断目标字符串是否存在。
- 如果需要更精确的结构信息,使用
deepGetHtml获取带注释标记的 HTML。
参考路径:
- src/content/shadow-dom-utils.js#L135-L173
- src/content/shadow-dom-utils.js#L183-L232
- test/unit/completion-evidence-content.unit.test.js#L51-L57
示例三:填充 Shadow DOM 内部的表单
适用场景:React 受控组件、富文本编辑器、自定义下拉框。
推荐步骤:
- 优先使用
query_elements返回的 ref,避免手写脆弱选择器。 - 如果是标准 input/textarea,使用
setNativeValue设置值并派发input、change。 - 如果是
contenteditable或富文本编辑器,使用fillContentEditable。 - 如果是自定义下拉,使用
selectDropdown或先点击触发器再匹配选项。
参考路径:
- src/content/interaction-tools.js#L153-L241
- src/content/interaction-tools.js#L399-L476
- src/content/interaction-tools.js#L666-L739
示例四:获取元素在视口中的坐标
适用场景:工具栏、悬浮提示、截图标注需要知道元素位置。
推荐步骤:
- 使用
getElementViewportPosition获取元素中心点坐标。 - 如果目标是 Range,使用
getRangeViewportPosition。 - 注意 iframe 偏移会被自动累加。
参考路径:
示例五:生成包含 Shadow DOM 的路径选择器
适用场景:调试、日志、快照重建时需要表达元素在 Shadow Host 中的位置。
推荐步骤:
- 使用
getDeepSelector生成路径。 - 如果元素在 Shadow Root 中,返回值会以
shadow(...)前缀开头。 - 消费方可根据前缀决定是否重新解析 Shadow Host。
参考路径:
章节来源
- src/content/shadow-dom-utils.js#L12-L45
- src/content/shadow-dom-utils.js#L135-L173
- src/content/shadow-dom-utils.js#L183-L232
- src/content/shadow-dom-utils.js#L239-L284
- src/content/shadow-dom-utils.js#L311-L377
- src/content/interaction-tools.js#L106-L148
- src/content/interaction-tools.js#L153-L241
- src/content/interaction-tools.js#L399-L476
- src/content/interaction-tools.js#L666-L739
- test/unit/completion-evidence-content.unit.test.js#L51-L57
性能、缓存与内存泄漏防护
遍历优化
- 最大递归深度默认限制为 5,避免深层嵌套导致栈溢出或长时间遍历。
deepQuerySelector在 Shadow DOM 中优先返回第一个匹配项,减少不必要遍历。deepQuerySelectorAll使用Set去重,避免同一元素被多次加入结果。deepGetText和deepGetHtml使用visitedSet 防止循环引用。
章节来源
- src/content/shadow-dom-utils.js#L12-L13
- src/content/shadow-dom-utils.js#L56-L86
- src/content/shadow-dom-utils.js#L135-L137
- src/content/shadow-dom-utils.js#L183-L185
缓存策略
page-interaction.js使用WeakMap将 Element 映射到 ref 编号,实现跨快照复用稳定编号。elementRegistry保存最近一次快照的 ref 到元素信息的映射,便于快速解析和兜底重查。- selector 懒生成:仅在需要时才调用
generateUniqueSelector,降低注册阶段开销。
章节来源
内存泄漏防护
attachSelectionListeners返回已添加监听器的集合,配合removeSelectionListeners统一移除。- 监听器对象包含 root 和 listener 引用,移除后清空集合,避免残留引用。
- Shadow DOM 选择变化监听器递归挂载,但通过
listeners.has(root)防止重复挂载。
章节来源
大页面与大数据风险
deepGetHtml会拼接 Shadow DOM 和 iframe 的完整 HTML,可能产生巨大字符串。deepGetText会合并多层文本,适合摘要或判断,不适合直接传给大型上下文。- 建议对长页面使用分页、截断、只读必要字段等策略。
章节来源
与现代前端框架的集成实践
React
- React 使用合成事件系统,因此直接调用
element.click()有时不足以触发受控组件状态更新。 - 本项目在表单填充和键盘输入中显式派发
input、change、keydown、keyup等事件,并尽量使用InputEvent携带inputType。 - 对于受控组件,优先使用
setNativeValue设置原生值,再派发事件,绕过 React 内部状态隔离。
推荐实践:
- 优先使用
query_elements返回的 ref,避免手写选择器。 - 输入类操作使用
fillForm或keyboardInput。 - 点击类操作使用
clickElement或clickByText。
参考路径:
Vue
- Vue 模板编译后通常生成真实 DOM,但第三方 UI 库可能使用 Shadow DOM。
- 对于第三方组件,优先使用
deepQuerySelector或deepQuerySelectorAll。 - 对于 Vue 自身组件,优先通过 ref 或组件暴露的方法;若只能操作 DOM,则使用本项目提供的工具。
参考路径:
Angular
- Angular 组件通常不强制使用 Shadow DOM,但 Material 等库可能引入隔离样式或自定义元素。
- 对于自定义元素,使用
deepQuerySelector穿透。 - 对于表单控件,使用
fillForm的checkbox、radio、select分支,避免 Angular 双向绑定失效。
参考路径:
通用最佳实践
- 优先使用
query_elements生成的 ref,而不是手写 CSS 选择器。 - 对复杂组件,先用
deepGetText或deepGetHtml观察内部结构。 - 对受控组件,优先设置原生值并派发事件,不要只改
value。 - 对 Shadow DOM 内部选择,使用
deepQuerySelectorAll并过滤可见元素。 - 对跨域 iframe,接受“不可访问”的降级结果,不要强行绕过浏览器安全策略。
章节来源
- src/content/interaction-tools.js#L172-L225
- src/content/interaction-tools.js#L431-L444
- src/content/shadow-dom-utils.js#L12-L45
故障排查指南
常见问题一:选择器找不到元素
现象:deepQuerySelector 返回 null。
可能原因:
- 元素在 Shadow DOM 内部,但选择器没有覆盖 Shadow Host。
- 元素尚未渲染完成。
- 元素被隐藏或不在可视区域。
- 元素在跨域 iframe 中。
排查建议:
- 使用
hasShadowDom判断页面是否包含 Shadow DOM。 - 使用
deepGetHtml查看 Shadow DOM 和 iframe 的 HTML 结构。 - 使用
query_elements获取结构化快照,确认元素是否被识别为可交互元素。 - 对 iframe,检查是否同源。
参考路径:
- src/content/shadow-dom-utils.js#L429-L443
- src/content/shadow-dom-utils.js#L183-L232
- src/content/page-interaction.js#L410-L440
常见问题二:点击无效或状态未更新
现象:点击成功,但 React/Vue/Angular 组件状态未更新。
可能原因:
- 只调用了
click(),没有派发框架依赖的事件。 - 使用了 jQuery 或旧式赋值方式,绕过了框架状态。
- 目标元素是
contenteditable或富文本编辑器。
排查建议:
- 使用
clickElement或interactByRef。 - 对输入类操作使用
fillForm或keyboardInput。 - 对富文本编辑器使用
fillContentEditable。
参考路径:
- src/content/interaction-tools.js#L106-L148
- src/content/interaction-tools.js#L172-L183
- src/content/interaction-tools.js#L399-L476
常见问题三:选择区域工具栏不响应 Shadow DOM 选择
现象:用户在 Shadow DOM 内部选中文本,但工具栏没有更新。
可能原因:
- 选择变化监听器未挂载到 Shadow Root。
- 监听器被提前移除。
- 当前文档不是 Shadow Root。
排查建议:
- 使用
attachSelectionListeners递归挂载监听器。 - 使用
removeSelectionListeners清理监听器。 - 使用
deepGetSelection获取 Shadow DOM 内的选择内容。
参考路径:
常见问题四:跨域 iframe 无法读取内容
现象:遍历到 iframe 后停止或报错。
可能原因:
- iframe 跨域,浏览器阻止访问
contentDocument。 - 扩展权限未配置或页面策略禁止嵌入。
排查建议:
- 接受跨域 iframe 不可访问的限制。
- 只对同源 iframe 进行深度遍历。
- 如需跨域访问,应通过后端代理或官方 API,而不是绕过浏览器安全策略。
参考路径:
章节来源
- src/content/shadow-dom-utils.js#L95-L125
- src/content/shadow-dom-utils.js#L183-L232
- src/content/shadow-dom-utils.js#L388-L423
- src/content/interaction-tools.js#L106-L148
- src/content/interaction-tools.js#L172-L183
- src/content/interaction-tools.js#L399-L476
- src/content/page-interaction.js#L410-L440
结论
本仓库的 Shadow DOM 穿透能力以 shadow-dom-utils.js 为核心,向上支撑了元素查询、文本提取、HTML 快照、选择区域、可见性判断、滚动收集和 ref 驱动的元素操作。其设计重点包括:
- 递归遍历 Shadow Root 和同源 iframe。
- 对跨域和权限受限场景做安全降级。
- 通过事件派发和原生值设置兼容现代前端框架。
- 使用 WeakMap、Set、懒生成 selector 等方式控制性能和内存。
- 将 Shadow DOM 能力抽象为可复用的工具模块,避免各功能模块重复实现。
对于使用者而言,最稳妥的实践是:优先使用 query_elements 返回的 ref,其次使用 deepQuerySelector 和 deepQuerySelectorAll,最后才手写选择器;对受控组件优先使用 fillForm、keyboardInput、clickElement 等高层工具,而不是直接操作 DOM。这样既能穿透 Shadow DOM,又能保持与 React、Vue、Angular 等框架的良好兼容性。