Skip to content

Content Scripts 注入机制 ​

本文引用的源码文件

本文引用的文件

更新摘要 ​

已进行的变更

  • 新增选择工具栏顶层层管理章节,详细说明 Popover API 的使用和 top layer 迁移
  • 添加遮挡检测与交互防护机制说明
  • 更新 z-index 冲突解决方案,特别是针对飞书等宿主应用的兼容性处理
  • 增强浮层生命周期管理和状态同步机制
  • 补充样式注入的安全考虑和 UA 默认样式处理

目录 ​

  1. 引言
  2. 项目结构与注入入口
  3. 核心组件总览
  4. 架构概览:Content Script 与扩展端通信
  5. 详细组件分析
  6. 依赖关系分析
  7. 性能优化与内存管理
  8. 安全考虑
  9. 故障排查指南
  10. 结论
  11. 附录:开发新页面工具实践

引言 ​

本文围绕扩展的 Content Script 注入机制展开,重点解释以下方面:

  • Content Script 的加载时机、作用域隔离与跨 frame 行为。
  • 与页面 DOM 的访问权限控制,包括 Shadow DOM 穿透和同源 iframe 限制。
  • 消息路由表实现:Map O(1) 查找、异步 Promise 处理、错误传播与顶层 frame 保护。
  • 页面工具集合的统一接口设计:信息提取、交互操作、数据抓取。
  • 新增:选择工具栏的顶层层管理,使用 Popover API 解决 z-index 冲突问题。
  • Shadow DOM 穿透技术与样式注入方案。
  • 安全策略:XSS 防护、权限最小化、恶意内容检测。
  • 性能优化:防抖节流、批量操作、DOM 签名自动等待、快照渲染预算控制。
  • 如何基于现有结构开发新的页面工具,并处理复杂网页结构和异步操作。

项目结构与注入入口 ​

Content Script 由 Manifest V3 的 content_scripts 字段声明,匹配所有 URL,在 document_idle 阶段注入,启用 all_frames,并包含 qrcode 与 marked 等库脚本。

graph TB
Manifest["manifest.json<br/>content_scripts 配置"] --> Loader["浏览器 Content Script 加载器"]
Loader --> QRCode["libs/qrcode.min.js"]
Loader --> Marked["libs/marked.min.js"]
Loader --> IndexJS["src/content/index.js"]
IndexJS --> PageTools["page-tools.js"]
IndexJS --> PageExtract["page-extract.js"]
IndexJS --> PageInteraction["page-interaction.js"]
IndexJS --> InteractionTools["interaction-tools.js"]
IndexJS --> AdvancedTools["advanced-tools.js"]
IndexJS --> SelectionToolbar["selection-toolbar.js"]
IndexJS --> ShadowUtils["shadow-dom-utils.js"]

图表来源

章节来源

注入时机与作用域 ​

  • 加载时机:run_at: document_idle,表示 DOM 已构建完成,但图片等资源可能仍在加载。
  • 作用域:每个 frame 独立执行,all_frames: true 使子帧也拥有独立上下文。
  • 全局变量隔离:Content Script 不能直接访问页面 JS 变量;如需访问,必须通过消息通道或注入函数。
  • 跨 frame 限制:同源 iframe 可被读取,跨源 iframe 会触发权限异常并被捕获跳过。

章节来源

核心组件总览 ​

Content Script 的核心职责是:

  • 提供统一的消息路由表,将后台或侧边栏请求分发到具体工具函数。
  • 封装页面读取、交互、提取、高级工具四类能力。
  • 新增:管理选择工具栏的顶层层显示和遮挡检测。
  • 维护元素引用注册表(ref),避免模型编写脆弱 CSS selector。
  • 支持 Shadow DOM 与 iframe 的深度查询与文本/HTML 聚合。
  • 提供样式注入、二维码生成、完成度校验等高级功能。
classDiagram
class ContentIndex {
+HANDLERS Map
+ASYNC_HANDLERS Set
+TOP_FRAME_ONLY_TYPES Set
+onMessage(message, sender, sendResponse)
+startRegionSelection() Promise
}
class SelectionToolbar {
+toolbarEl Element
+resultPanelEl Element
+overflowDropdownEl Element
+openFloat(el, displayValue)
+closeFloat(el)
+probeFloatOcclusion(el)
+releaseOcclusionIfIdle()
+showPopover()
+hidePopover()
}
class PageTools {
+getPageText(options)
+getFullHtml(options)
+getSelectedContent(format)
+extractTable(selector, includeHeaders, format)
+copyToClipboard(text)
+pasteFromClipboard()
+hoverElement(selector)
+highlightText(text, color)
}
class PageExtract {
+extractMetadata()
+extractLinks(filterType, includeImages)
+extractForms(formSelector)
+extractImages(options)
+searchInPage(options)
+getIframeContent(selector, includeNested, maxLength)
}
class PageInteraction {
+queryInteractiveElements(options)
+collectSnapshotOps(options)
+isFrameVisible()
+scrollAndCollect(args)
+interactByRef(ref, action, options)
+resolveByRef(ref)
}
class InteractionTools {
+clickElement(selector, waitTime, timeout)
+fillForm(fields, waitTime)
+scrollToPosition(options)
+waitForElement(selector, state, timeout)
+keyboardInput(params)
+fileUpload(selector, fileName, fileContent, fileType)
+selectDropdown(triggerSelector, optionText, optionSelector, timeout)
+manageStorage(params)
+clickByText(text, options)
}
class AdvancedTools {
+generateQRCode(content, size, errorCorrection, showImage)
+injectCss(css, targetSelector, injectMode)
+evaluateDoneCriteria(criteria, doc, loc)
+sanitizeInjectedCss(css)
}
class ShadowDomUtils {
+deepQuerySelector(selector, root, maxDepth, depth)
+deepQuerySelectorAll(selector, root, maxDepth, depth, found)
+deepGetText(root, maxDepth, depth, visited)
+deepGetHtml(root, maxDepth, depth, visited)
+deepGetSelection(root, maxDepth, depth)
}
ContentIndex --> PageTools : "调用"
ContentIndex --> PageExtract : "调用"
ContentIndex --> PageInteraction : "调用"
ContentIndex --> InteractionTools : "调用"
ContentIndex --> AdvancedTools : "调用"
ContentIndex --> SelectionToolbar : "管理"
PageTools --> ShadowDomUtils : "使用"
PageExtract --> ShadowDomUtils : "使用"
PageInteraction --> ShadowDomUtils : "使用"
InteractionTools --> ShadowDomUtils : "使用"
SelectionToolbar --> ShadowDomUtils : "使用"

图表来源

章节来源

架构概览:Content Script 与扩展端通信 ​

Content Script 通过 chrome.runtime.onMessage 监听消息,由 HANDLERS Map 分发到对应处理器。异步处理器返回 Promise,由路由层统一 Promise.resolve(result).then(sendResponse) 处理。

sequenceDiagram
participant Background as "Background / Side Panel"
participant Content as "Content Script (index.js)"
participant Handler as "HANDLERS Map"
participant Tool as "具体工具函数"
Background->>Content : chrome.runtime.sendMessage({type, payload})
Content->>Content : 检查 TOP_FRAME_ONLY_TYPES
Content->>Handler : HANDLERS[type]
Handler-->>Content : handler 函数
Content->>Tool : 调用工具函数
Tool-->>Content : 同步结果 或 Promise
Content->>Content : ASYNC_HANDLERS 判断
alt 异步
Content->>Content : Promise.resolve(result).then(sendResponse)
Content-->>Background : sendResponse(data)
else 同步
Content-->>Background : sendResponse(result)
end

图表来源

章节来源

详细组件分析 ​

消息路由表与异步处理 ​

  • HANDLERS 是一个普通对象,键为消息类型,值为处理器函数。JavaScript 对象属性查找近似 O(1),适合高频消息分发。
  • ASYNC_HANDLERS 是一个 Set,标记需要保持消息通道开放的异步工具。
  • TOP_FRAME_ONLY_TYPES 限制某些工具只在顶层 frame 响应,避免 iframe 抢先应答导致取证或 ref 解析混乱。
flowchart TD
Start(["收到消息"]) --> CheckTop["是否 TOP_FRAME_ONLY_TYPES 且非顶层 frame?"]
CheckTop --> |是| Ignore["忽略消息"]
CheckTop --> |否| FindHandler["从 HANDLERS 查找处理器"]
FindHandler --> HasHandler{"找到处理器?"}
HasHandler --> |否| End["结束"]
HasHandler --> |是| CallHandler["调用处理器"]
CallHandler --> IsAsync{"是否异步?"}
IsAsync --> |是| ResolvePromise["Promise.resolve().then(sendResponse)"]
IsAsync --> |否| SendSync["sendResponse(同步结果)"]
ResolvePromise --> End
SendSync --> End

图表来源

章节来源

选择工具栏顶层层管理与遮挡检测 ​

新增 选择工具栏现已采用 Popover API 进入浏览器的 top layer,解决了传统 z-index 层级冲突问题,特别是在飞书等同文档应用中。

顶层层管理机制 ​

  • Popover API 集成:工具栏、溢出下拉菜单、结果面板和 Token 弹窗都使用 popover="manual" 属性,通过 showPopover() 和 hidePopover() 方法控制显示状态。
  • 回退机制:在不支持 Popover API 的环境中,自动回退到传统的 body 挂载 + 内联 display 方式。
  • 状态同步:使用 isFloatOpen() 函数确保 popover 状态与内联 display 状态保持一致。
flowchart TD
Start(["显示浮层"]) --> CheckAPI{"是否支持 Popover API?"}
CheckAPI --> |是| ShowPopover["调用 showPopover()"]
CheckAPI --> |否| FallbackDisplay["设置 display 属性"]
ShowPopover --> SetDisplay["同时设置 display 属性"]
SetDisplay --> ProbeOcclusion["探测遮挡元素"]
FallbackDisplay --> ProbeOcclusion
ProbeOcclusion --> HitTest["elementFromPoint 命中测试"]
HitTest --> FoundOccluder{"发现外来遮挡元素?"}
FoundOccluder --> |是| TempHide["临时隐藏遮挡元素"]
FoundOccluder --> |否| NoAction["无需操作"]
TempHide --> TrackHidden["记录被隐藏的遮挡元素"]
NoAction --> ReturnSuccess["返回成功"]
TrackHidden --> ReturnSuccess

图表来源

遮挡检测与交互防护 ​

  • 智能遮挡检测:使用 elementFromPoint 在浮层关键位置进行命中测试,检测是否有宿主页面的高层级元素遮挡。
  • 临时遮挡处理:发现外来遮挡元素时,将其临时隐藏并记录原始 display 状态,在所有浮层关闭后自动恢复。
  • 自身元素豁免:通过 data-aih 标记识别扩展自身的浮层元素,避免误伤。
  • 交互窗口期保护:在用户与工具栏交互期间(mousedown→mouseup),防止宿主页面清选区导致的工具栏意外隐藏。
flowchart TD
MouseClick["用户点击事件"] --> CheckInteracting{"是否在交互窗口期内?"}
CheckInteracting --> |是| SuppressHide["抑制工具栏隐藏"]
CheckInteracting --> |否| NormalFlow["正常流程"]
SuppressHide --> ProcessAction["处理点击动作"]
NormalFlow --> CheckSelection{"选区是否存在?"}
CheckSelection --> |否| HideToolbar["隐藏工具栏"]
CheckSelection --> |是| ShowToolbar["显示工具栏"]
ProcessAction --> Complete["完成"]
HideToolbar --> Complete
ShowToolbar --> Complete

图表来源

z-index 冲突解决方案 ​

  • CSS 最大值策略:工具栏使用 z-index: 2147483647(CSS 最大值)确保最高优先级。
  • top layer 优势:通过 Popover API 进入 top layer,绕过传统 z-index 层级系统。
  • 宿主应用兼容:专门针对飞书等高 z-index 宿主应用进行优化,确保扩展浮层始终可见。

章节来源

页面工具集合:信息提取、交互操作、数据抓取 ​

  • 信息提取:page-extract.js 提供元数据、链接、表单、图片、Markdown、JSON、iframe 内容提取。
  • 交互操作:interaction-tools.js 提供点击、填充表单、滚动、等待元素、键盘输入、文件上传、下拉选择、存储管理。
  • 数据抓取:page-tools.js 提供页面文本、完整 HTML、选中内容、表格提取、剪贴板操作、悬停、高亮。
graph LR
Extract["page-extract.js<br/>信息提取"] --> Tools["统一工具接口"]
Interaction["interaction-tools.js<br/>交互操作"] --> Tools
PageTools["page-tools.js<br/>数据抓取"] --> Tools
Tools --> Model["模型/Agent 调用"]

图表来源

章节来源

元素引用注册表与 ref 解析 ​

  • elementRegistry 是 Map<ref, {element, selector, tag, role, name}>,用于稳定引用元素。
  • elementRefMap 是 WeakMap<Element, ref>,跨快照复用编号,元素 GC 后编号退役。
  • resolveByRef 提供统一解析逻辑:注册表命中 → isConnected 检查 → selector 兜底重查 → 失败时返回结构化建议。
flowchart TD
Start(["resolveByRef(ref)"]) --> Parse["解析 ref 数字"]
Parse --> HasEntry{"elementRegistry.has(ref)?"}
HasEntry --> |否| ReturnError["返回错误 + 建议"]
HasEntry --> |是| GetEntry["获取 entry"]
GetEntry --> Connected{"entry.element.isConnected?"}
Connected --> |是| EnsureSel["ensureSelector(entry)"]
Connected --> |否| Fallback["selector 兜底重查"]
EnsureSel --> ReturnEntry["返回 {entry, element}"]
Fallback --> Found{"找到元素?"}
Found --> |是| UpdateEntry["更新 entry.element"]
Found --> |否| ReturnError
UpdateEntry --> ReturnEntry

图表来源

章节来源

Shadow DOM 穿透技术 ​

  • deepQuerySelector 递归遍历 Shadow DOM 和同源 iframe,优先深层匹配。
  • deepGetText 和 deepGetHtml 聚合 Shadow DOM 与 iframe 内容,防止循环引用。
  • attachSelectionListeners 在所有 Shadow DOM 上附加选择变化监听器。
flowchart TD
Start(["deepQuerySelector(selector, root)"]) --> DepthCheck{"depth > maxDepth?"}
DepthCheck --> |是| ReturnNull["返回 null"]
DepthCheck --> |否| QueryShadow["遍历 shadowRoot"]
QueryShadow --> QueryIframe["遍历 iframe"]
QueryIframe --> QueryCurrent["root.querySelector(selector)"]
QueryCurrent --> Found{"找到元素?"}
Found --> |是| ReturnEl["返回元素"]
Found --> |否| ReturnNull

图表来源

章节来源

样式注入与安全净化 ​

  • injectCss 支持 style 和 inline 两种模式,style 模式支持 scoped 注入。
  • sanitizeInjectedCss 过滤危险 CSS 构造:@import、url()、expression()、-moz-element()、behavior 等。
  • 注入前统一净化,确保不执行脚本、不外泄数据。
  • 新增:Popover API 相关的 UA 默认样式处理,避免覆盖组件自身样式。
flowchart TD
Start(["injectCss(css, targetSelector, injectMode)"]) --> Sanitize["sanitizeInjectedCss(css)"]
Sanitize --> ModeCheck{"injectMode 合法?"}
ModeCheck --> |否| ReturnError["返回错误"]
ModeCheck --> |是| StyleMode{"injectMode === 'style'?"}
StyleMode --> |是| ScopedOrGlobal["scoped 或 global 注入"]
StyleMode --> |否| InlineMode["inline 属性注入"]
ScopedOrGlobal --> ReturnSuccess["返回成功"]
InlineMode --> ReturnSuccess

图表来源

章节来源

页面快照渲染与分页 ​

  • renderSnapshot 是统一渲染出口,支持分页、字符预算、iframe 区块、打开层与主体分区。
  • collectSnapshotOps 采集结构化树,renderSnapshot 输出文本快照。
flowchart TD
Start(["collectSnapshotOps(options)"]) --> CollectMatches["collectMatches(document.body)"]
CollectMatches --> RegisterElements["registerElement(el)"]
RegisterElements --> BuildTree["buildTree(document.body)"]
BuildTree --> ReturnOps["返回 {overlayTrees, bodyTree, frameInfo}"]
ReturnOps --> RenderSnapshot["renderSnapshot(frames, page, maxChars)"]
RenderSnapshot --> Output["输出文本快照"]

图表来源

章节来源

依赖关系分析 ​

Content Script 内部模块依赖清晰:

  • index.js 作为入口,依赖所有工具模块。
  • page-tools.js 依赖 shadow-dom-utils.js 和 page-utils.js。
  • page-extract.js 依赖 shadow-dom-utils.js 和 page-utils.js。
  • page-interaction.js 依赖 shadow-dom-utils.js、page-utils.js 和 page-snapshot-renderer.js。
  • interaction-tools.js 依赖 shadow-dom-utils.js、page-utils.js 和 page-interaction.js。
  • advanced-tools.js 依赖 shadow-dom-utils.js。
  • 新增:selection-toolbar.js 依赖 shadow-dom-utils.js 和 selection-toolbar-styles.js。
graph TB
Index["index.js"] --> PageTools["page-tools.js"]
Index --> PageExtract["page-extract.js"]
Index --> PageInteraction["page-interaction.js"]
Index --> InteractionTools["interaction-tools.js"]
Index --> AdvancedTools["advanced-tools.js"]
Index --> SelectionToolbar["selection-toolbar.js"]
PageTools --> ShadowUtils["shadow-dom-utils.js"]
PageTools --> PageUtils["page-utils.js"]
PageExtract --> ShadowUtils
PageExtract --> PageUtils
PageInteraction --> ShadowUtils
PageInteraction --> PageUtils
PageInteraction --> SnapshotRenderer["page-snapshot-renderer.js"]
InteractionTools --> ShadowUtils
InteractionTools --> PageUtils
InteractionTools --> PageInteraction
AdvancedTools --> ShadowUtils
SelectionToolbar --> ShadowUtils
SelectionToolbar --> ToolbarStyles["selection-toolbar-styles.js"]

图表来源

章节来源

性能优化与内存管理 ​

  • 防抖节流:autoWaitAfterAction 使用最小等待和稳定计数,避免频繁轮询。
  • 批量操作:fillForm 批量填充表单字段,返回每个字段的处理结果。
  • 内存管理:elementRegistry.clear() 每次快照重建,WeakMap 允许元素 GC 后编号退役。
  • DOM 签名:getDomSignature 轻量签名,检测导航和 DOM 变化。
  • 快照渲染预算:renderSnapshot 使用 maxChars 控制输出长度,避免大页面阻塞。
  • 新增:浮层遮挡检测使用采样点策略,避免全量扫描提升性能。
flowchart TD
Start(["autoWaitAfterAction(sigBefore, minWait, maxWait)"]) --> MinWait["等待 minWait"]
MinWait --> CheckSig["计算 getDomSignature()"]
CheckSig --> Changed{"签名变化?"}
Changed --> |是| ResetStable["stableCount = 0"]
Changed --> |否| IncStable["stableCount++"]
ResetStable --> Loop{"stableCount >= 2?"}
IncStable --> Loop
Loop --> |否| WaitNext["等待 100ms"]
Loop --> |是| ReturnResult["返回 {changed, urlChanged, domChanged, waitedMs}"]
WaitNext --> CheckSig

图表来源

章节来源

安全考虑 ​

  • XSS 防护:sanitizeInjectedCss 过滤危险 CSS 构造,防止脚本执行和数据外泄。
  • 权限最小化:Manifest 仅声明必要权限,如 activeTab、storage、scripting 等。
  • 恶意内容检测:searchInPage 使用正则转义,避免注入恶意模式;highlightTextMatches 仅包裹文本节点,不修改元素结构。
  • 上下文失效保护:全局 error 和 unhandledrejection 监听屏蔽 Extension context invalidated 错误。
  • 新增:Popover API 相关的安全考虑,确保 UA 默认样式不会覆盖组件自身样式。
flowchart TD
Start(["sanitizeInjectedCss(css)"]) --> FilterImport["过滤 @import"]
FilterImport --> FilterUrl["过滤 url()"]
FilterUrl --> FilterExpression["过滤 expression()"]
FilterExpression --> FilterMozElement["过滤 -moz-element()"]
FilterMozElement --> FilterBehavior["过滤 behavior"]
FilterBehavior --> ReturnSafe["返回安全 CSS"]

图表来源

章节来源

故障排查指南 ​

  • Content Script 未注入:检查 manifest.json 的 content_scripts 配置,确认 matches 和 run_at。
  • 消息无响应:检查 HANDLERS 中是否有对应处理器,确认 ASYNC_HANDLERS 是否正确标记。
  • ref 解析失败:调用 query_elements 重新获取快照,检查 elementRegistry 是否过期。
  • Shadow DOM 无法访问:确认目标元素是否在 Shadow DOM 内,使用 deepQuerySelector 而非原生 querySelector。
  • 样式注入无效:检查 sanitizeInjectedCss 是否过滤了关键 CSS,确认 injectMode 是否合法。
  • 新增:工具栏显示问题,检查 Popover API 支持情况和遮挡检测逻辑。
  • 新增:z-index 冲突问题,确认工具栏是否正确使用 top layer 机制。

章节来源

结论 ​

Content Script 注入机制通过 Manifest 配置、消息路由表、工具模块化和 Shadow DOM 穿透技术,实现了安全、高效、可扩展的页面交互能力。新增的选择工具栏顶层层管理机制进一步增强了在现代网页环境中的兼容性和用户体验。其设计强调:

  • 清晰的模块职责分离。
  • 稳定的元素引用机制。
  • 安全的样式注入和内容过滤。
  • 新增:先进的浮层层级管理和遮挡检测。
  • 高性能的异步处理和内存管理。
  • 完善的错误处理和故障恢复。

这些特性使得 Content Script 能够应对复杂的现代网页结构,同时保证安全性和性能,特别是在飞书等高复杂度宿主应用中表现出色。

附录:开发新页面工具实践 ​

步骤一:定义工具函数 ​

在对应模块中定义工具函数,例如在 page-tools.js 中添加 myNewTool。

javascript
export function myNewTool(options) {
  // 实现逻辑
  return { success: true, data: result };
}

章节来源

步骤二:注册消息处理器 ​

在 index.js 的 HANDLERS 中注册新工具。

javascript
const HANDLERS = {
  MY_NEW_TOOL: (msg) => myNewTool(msg),
  // ... 其他处理器
};

章节来源

步骤三:处理异步操作 ​

如果工具是异步的,添加到 ASYNC_HANDLERS。

javascript
const ASYNC_HANDLERS = new Set([
  'MY_NEW_TOOL',
  // ... 其他异步工具
]);

章节来源

步骤四:测试与调试 ​

  • 使用 Chrome DevTools 的 Console 面板调试 Content Script。
  • 通过 chrome.runtime.sendMessage 发送测试消息。
  • 检查控制台日志和返回值。

章节来源