rrweb 跨域 iframe 录制完整指南:recordCrossOriginIframes 配置、事件桥接原理与四种注入方案

发布时间:2026/9/20 2:22:15
rrweb 跨域 iframe 录制完整指南:recordCrossOriginIframes 配置、事件桥接原理与四种注入方案 rrweb 跨域 iframe 录制完整指南recordCrossOriginIframes 配置、事件桥接原理与四种注入方案【免费下载链接】rrwebrecord and replay the web项目地址: https://gitcode.com/gh_mirrors/rr/rrweb跨域 iframe 是网页录制中的典型难题浏览器同源策略默认禁止父页面读取不同域 iframe 的 DOM 内容。rrweb 通过recordCrossOriginIframes配置项配合postMessage事件桥接实现了对跨域 iframe 内容的完整录制与回放。本文以 docs/recipes/cross-origin-iframes.md 为主体结合 rrweb 仓库源码深入讲解该特性的配置方式、底层实现原理、安全注意事项并给出网站所有者、浏览器扩展、Puppeteer、Electron 四种注入 rrweb 的实战方案。为什么跨域 iframe 难以录制默认情况下浏览器很难访问托管在不同域上的 iframe 的内容。这是浏览器的一项安全功能目的是防止恶意站点访问其他站点上的敏感信息。同源 iframeiframe.contentDocument可访问rrweb 可以直接观测但一旦 iframe 的域名与父页面不同父页面就无法触碰其 DOM录制自然无从谈起。rrweb 提供了一套解决方案但必须充分理解其安全代价后再决定是否启用。如果你对网站的安全有严格要求只允许你信任的网站将你的网站嵌入 iframe 中官方并不建议使用该方案。原因很直接一旦允许录制跨源 iframe任何恶意网站都可以把你的网站嵌入它自己的页面只要它在嵌入页面里运行 rrweb就能记录你网站的全部内容。核心配置recordCrossOriginIframesrecordCrossOriginIframes是record函数recordOptions中的一个布尔配置项定义位于 packages/rrweb/src/types.ts#L71默认值为false见 packages/rrweb/src/record/index.ts#L91即默认不启用跨域 iframe 录制。父页面开启录制在父页面中启用录制跨域 iframe所有事件包括来自跨源 iframe 的事件都会在emit回调中收到import { record } from rrweb/record; record({ emit(event) {}, // 所有事件都将在此处发出包括来自跨源 iframe 的事件 recordCrossOriginIframes: true, });子页面配合启用在子页面即被嵌入的跨域页面中也需要开启该配置。emit回调对 rrweb 来说是必需的参数但子页面本身不会发出任何事件——它的职责是把自己的录制结果上报给父窗口import { record } from rrweb/record; record({ emit(event) {}, // 这是 rrweb 所必需的但子页面不会发出任何事件 recordCrossOriginIframes: true, });两个页面缺一不可父页面负责收集与汇聚事件子页面负责实际录制自身 DOM 并把事件上抛。底层原理postMessage 事件桥接跨域 iframe 录制之所以能成立关键在于 rrweb 在记录端把 iframe 内的事件通过window.postMessage转发给父窗口。从源码看这一机制分两层子页面侧判断是否处于顶层窗口并上抛事件在 packages/rrweb/src/record/index.ts#L106-L120 中开启recordCrossOriginIframes后rrweb 会检查自己是否运行在顶层窗口const inEmittingFrame recordCrossOriginIframes ? window.parent window : true; let passEmitsToParent false; if (!inEmittingFrame) { try { // throws if parent is cross-origin if (window.parent.document) { passEmitsToParent false; // 父窗口同源时事件由父窗口直接收集 } } catch (e) { passEmitsToParent true; } }这里有两层判断逻辑若当前窗口就是顶层窗口window.parent windowinEmittingFrame为true事件走正常的emit回调若当前窗口是 iframe 且父窗口为跨域访问window.parent.document会抛异常则passEmitsToParent为true事件不再调用本地的emit而是封装成消息通过window.parent.postMessage(message, *)发送给父窗口packages/rrweb/src/record/index.ts#L201-L211。桥接消息的格式如下const message: CrossOriginIframeMessageEventContentT { type: rrweb, event: eventProcessor(e), origin: window.location.origin, isCheckout, };消息体包含type固定为rrweb用于标识、event事件本身、origin子页面的源用于父窗口校验和isCheckout是否为结算快照。另外需要注意的是事件在发送给父窗口前不会经过packFn打包——源码中eventProcessor在passEmitsToParent为真时会跳过packFnpackages/rrweb/src/record/index.ts#L176-L182因为打包后的字符串无法在父窗口侧做 ID 重映射处理。父页面侧IframeManager 接收并转换事件父页面的录制入口通过IframeManager来管理跨域 iframe 事件。当recordCrossOriginIframes开启时构造函数会注册window.addEventListener(message, this.handleMessage.bind(this))见 packages/rrweb/src/record/iframe-manager.ts#L46-L49。handleMessage对收到的消息做三道过滤packages/rrweb/src/record/iframe-manager.ts#L98-L123消息的data.type必须为rrweb校验crossOriginMessageEvent.origin crossOriginMessageEvent.data.origin防止其他站点转发的伪造 rrweb 消息通过message.source在crossOriginIframeMap中反查出对应的 iframe 元素。通过过滤后事件进入transformCrossOriginEvent做关键转换packages/rrweb/src/record/iframe-manager.ts#L125-L252FullSnapshot 转换跨域 iframe 发出的 full snapshot 会被改写为一条IncrementalSnapshotMutation事件携带isAttachIframe: true以挂载 iframe的形式合入父页面的事件流ID 重映射iframe 内部的事件 IDid、parentId、nextId、previousId、styleId等通过CrossOriginIframeMirror映射为父窗口事件流中全局唯一的 ID防止与父页面自身节点 ID 冲突选择性丢弃Meta、Load、DomContentLoaded以及ViewportResize等事件会被直接忽略return false因为它们在父页面语境下没有意义或会造成重复透传与改写Plugin、Custom事件以及各类增量事件Mutation、MouseMove、Scroll、Input、MediaInteraction、CanvasMutation、StyleSheetRule、Selection、AdoptedStyleSheet 等在完成 ID 映射后被合并进父页面事件流。ID 映射的具体实现是 packages/rrweb/src/record/cross-origin-iframe-mirror.ts 中的CrossOriginIframeMirror类。它为每个 iframe 维护两张WeakMap映射表iframeIdToRemoteIdMap与iframeRemoteIdToIdMap在父子窗口两个 ID 命名空间之间做双向换算并在收到新的 full snapshot 时调用reset(iframeEl)清理旧映射保证映射状态与 iframe 实际内容一致。这一整套桥接链路在仓库测试中有完整的端到端验证packages/rrweb/test/record/cross-origin-iframes.test.ts 会通过 Puppeteer 递归地向主页面和所有子 frame 注入录制脚本统一开启recordCrossOriginIframes: true再断言父窗口收集到的事件快照packages/all/test/cross-origin-iframe-packer.test.ts 则验证了跨域桥接与packFn/unpackFn组合场景下的行为。注意事项与安全边界开启跨域 iframe 录制时务必清楚以下三点顶层窗口必须有 rrweb当跨源 iframe 录制开启时rrweb 会检查自身是否运行在顶层窗口。如果不是它会通过postMessage把事件发送给父窗口。如果你没有在顶层窗口中运行 rrweb打开recordCrossOriginIframes后事件将丢失——没有接收方消息只会石沉大海。恶意顶层窗口可以窃听如果顶层窗口是一个恶意网站它可以监听这些消息并把事件发送到它自己选择的服务器。这意味着你的页面在被恶意站点嵌入后其内容会被完整录制并泄露。postMessage 通信未加密如果恶意脚本运行在你的页面上它可以监听postMessage由于子窗口与父窗口之间的通信未加密这些脚本可以看到所有事件数据。因此启用该特性只适用于父页面与 iframe 双方都由你掌控的场景且需要在业务层面评估信任边界。将 rrweb 注入跨域 iframe 的四种方案跨域 iframe 录制的难点还在于子页面默认不会运行 rrweb。你需要在子页面的执行环境中注入录制脚本。以下是官方文档给出的四种可行路径。方案一网站所有者在 iframe 中添加 rrweb如果你同时拥有使用 iframe 的网站父页面和嵌入在 iframe 中的网站子页面最直接的方式就是通过script标签把 rrweb 同时加入两个页面。这是最可控的方案双方代码都在你手里配置、版本和上报逻辑可以统一管理配合上文父页面开启录制 子页面配合启用的两段配置即可完成接入。方案二浏览器扩展浏览器扩展可以利用 Manifest V3 的 content scripts 能力向页面及其子 frame 注入录制脚本。Chrome 扩展的 content scripts 默认只在顶层页面运行需要在 manifest 中配置all_frames: true才能让脚本进入所有子 frame进而为每个 frame 开启录制。仓库中的 packages/web-extension/src/content/index.ts 就是这一思路的完整参考实现它通过isInCrossOriginIFrame()判断当前执行环境若处于跨域 iframe 则走initCrossOriginIframe()分支否则在顶层窗口走initMainPage()分支发起录制时下发的配置正是recordCrossOriginIframes: true。同时它还监听message事件实现录制脚本与扩展的通信握手。如果你要基于浏览器扩展做页面录制可以对照该实现。方案三Puppeteer 脚本如果你在自动化测试或数据采集场景下需要录制跨域 iframe可以用 Puppeteer 控制浏览器在每个 frame 上动态注入 rrweb 代码。以下是官方文档给出的完整脚本import puppeteer from puppeteer; async function injectRecording(frame) { await frame.evaluate((rrwebCode) { if (window.__IS_RECORDING__) return; window.__IS_RECORDING__ true; (async () { function loadScript(code) { const s document.createElement(script); s.type text/javascript; s.innerHTML code; if (document.head) { document.head.append(s); } else { requestAnimationFrame(() { document.head.append(s); }); } } loadScript(rrwebCode); window.rrweb.record({ emit: (event) { window._captureEvent(event); }, recordCrossOriginIframes: true, }); })(); }, code); } const browser await puppeteer.launch(); const page (await browser.pages())[0]; const events []; // 包含来自所有 Frame 的所有事件 await page.exposeFunction(_captureEvent, (event) { events.push(event); }); page.on(framenavigated, async (frame) { await injectRecording(frame); // 将 rrweb 注入 iframe }); await page.goto(https://example.com); // 你的事件将在事件数组中脚本要点page.exposeFunction(_captureEvent, ...)把 Node.js 侧的回调暴露给浏览器环境iframe 内录到的事件通过window._captureEvent(event)传回 Node 侧的events数组page.on(framenavigated, ...)在每次 frame 导航完成后触发注入injectRecording会递归地对子 frame 执行frame.evaluate保证新出现的 iframe 也能拿到录制脚本window.__IS_RECORDING__标志位防止同一 frame 被重复注入loadScript通过document.createElement(script)innerHTML的方式同步加载代码并在document.head尚不存在时退回到requestAnimationFrame等待 DOM 就绪。方案四Electron在 Electron 应用中录制页面时可以通过 BrowserWindow 的preload机制把录制脚本注入所有 frameconst win new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, rrweb-recording-script.js), // 这会打开 iframe 内的预加载但会禁用节点集成 nodeIntegrationInSubFrames: true, nodeIntegration: false, }, });关键配置是nodeIntegrationInSubFrames: true它让 preload 脚本在 iframe 内也会执行同时保持nodeIntegration: false以关闭子 frame 的 Node.js 集成避免安全风险。这样rrweb-recording-script.js会在每个 frame 中运行配合recordCrossOriginIframes: true即可实现跨域 iframe 录制。Electron 的 preload 机制细节可参考官方教程preload 脚本与进程模型中 preload 的相关章节此处不展开。总结跨域 iframe 录制是 rrweb 针对浏览器同源策略限制提供的一项进阶能力子页面开启recordCrossOriginIframes后把事件经postMessage上抛父页面通过IframeManager接收、校验、重映射 ID 并汇入事件流最终在回放端得到包含 iframe 内容的完整还原。启用它需要同时满足顶层窗口运行 rrweb与信任父子双方页面两个前提并清醒认识到 postMessage 桥接存在的窃听风险。至于如何在子页面注入 rrweb可以按场景从网站双端注入、浏览器扩展、Puppeteer、Electron preload四种方案中选择。进一步阅读中文版文档docs/recipes/cross-origin-iframes.zh_CN.md配置项定义packages/rrweb/src/types.ts#L71桥接与顶层窗口判断packages/rrweb/src/record/index.ts#L106-L211父窗口事件接收与转换packages/rrweb/src/record/iframe-manager.tsID 双向映射实现packages/rrweb/src/record/cross-origin-iframe-mirror.ts端到端测试packages/rrweb/test/record/cross-origin-iframes.test.ts浏览器扩展实现参考packages/web-extension/src/content/index.ts【免费下载链接】rrwebrecord and replay the web项目地址: https://gitcode.com/gh_mirrors/rr/rrweb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考