Files
rrweb/docs/integration/superrpa-integration.zh_CN.md
zhaoyilun 27a17d7068
Some checks failed
Tests / Tests (push) Has been cancelled
ESLint Check / ESLint Check and Report Upload (push) Has been cancelled
Prettier Check / Format Check (push) Has been cancelled
Prettier Check / Format Code (push) Has been cancelled
ESLint Check / Build Base for Bundle Size Comparison (push) Has been cancelled
Add SuperRPA integration guide, simple extension and standalone replay page
- docs/integration/superrpa-integration.zh_CN.md: complete integration guide
- rrweb-simple-ext/: minimal Chrome extension for page recording
- replay.html: standalone drag-and-drop replay viewer
- CLAUDE.md: project instructions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 17:08:24 +08:00

820 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# rrweb 与 SuperRPA 集成适配指南
本文档面向 SuperRPA魔改版 Chromium项目开发团队说明如何将 rrweb 录制/回放能力集成到浏览器中。
---
## 1. rrweb 概述
rrweb 是一个 Web 页面操作录制与回放库。它通过以下机制工作:
1. **全量快照 (Full Snapshot)**:将当前页面 DOM 树序列化为一个带唯一 ID 的 JSON 数据结构
2. **增量快照 (Incremental Snapshot)**:通过 MutationObserver 和各种事件监听器,持续捕获 DOM 变更和用户交互
3. **回放 (Replay)**:从全量快照重建 DOM然后按时间戳顺序重放增量事件
所有录制产物都是纯 JSON 数据(`eventWithTime[]`),可序列化传输和持久存储。
### 录制覆盖的用户操作
| 类别 | 具体内容 |
|------|----------|
| DOM 变更 | 节点增删、属性变化、文本内容变化 |
| 鼠标操作 | 移动、点击、双击、右键、悬停 |
| 键盘输入 | 输入框文本变化、复选框/单选框状态 |
| 滚动 | 页面滚动、元素内滚动 |
| 触摸 | TouchStart、TouchMove、TouchEnd |
| Canvas | 2D 绑定调用记录、WebGL 调用记录 |
| 媒体 | 音视频播放、暂停、跳转、音量变化 |
| 样式 | CSS 规则增删改、行内样式变化 |
| 字体 | 动态加载的字体 |
| 文本选择 | 选区变化 |
| Shadow DOM | Shadow DOM 内的变更 |
| 自定义元素 | Custom Element 注册 |
| 跨域 iframe | 通过 postMessage 桥接录制 |
---
## 2. 构建产物
### 2.1 构建
```bash
# 在 rrweb 项目根目录
yarn install
yarn build:all
```
### 2.2 关键产物文件
| 文件 | 用途 | 大小参考 |
|------|------|----------|
| `packages/rrweb/dist/rrweb.umd.cjs` | 全量包(录制+回放UMD 格式 | 较大 |
| `packages/rrweb/dist/rrweb.js` | 全量包ESM 格式 | 较大 |
| `packages/record/dist/record.umd.cjs` | **仅录制**UMD 格式(推荐用于注入) | ~40KB gzip |
| `packages/record/dist/record.js` | 仅录制ESM 格式 | ~40KB gzip |
| `packages/replay/dist/replay.js` | 仅回放ESM 格式 | ~50KB gzip |
| `packages/replay/dist/style.css` | 回放必需的 CSS 样式 | — |
| `packages/rrweb/dist/style.css` | 全量包回放样式 | — |
**建议**SuperRPA 注入录制脚本时,使用 `@rrweb/record`(仅录制)以减小注入体积。回放端使用 `@rrweb/replay``rrweb-player`
### 2.3 UMD 全局变量名
注入后rrweb 的 API 会挂载到全局变量上:
- `rrweb.umd.cjs``window.rrweb`(包含 `record``Replayer`
- `record.umd.cjs``window.rrwebRecord`(仅包含 `record`
---
## 3. 集成方案
SuperRPA 作为魔改版 Chromium有以下几种集成方式
### 方案 A通过 CDP 注入(推荐,改动最小)
利用 Chrome DevTools Protocol 的 `Page.addScriptToEvaluateOnNewDocument` 在每个页面加载前注入录制脚本。
```
┌─────────────────────────────────────────────┐
│ SuperRPA (Chromium) │
│ │
│ ┌──────────┐ CDP ┌───────────────┐ │
│ │ 控制层 │ ────────→ │ 渲染进程 │ │
│ │ (Browser │ │ │ │
│ │ Process)│ │ Web Page │ │
│ │ │ │ ┌──────────┐ │ │
│ │ 收集事件 │ ←──────── │ │ rrweb │ │ │
│ │ 存储/转发 │ postMsg │ │ record() │ │ │
│ └──────────┘ │ └──────────┘ │ │
│ └───────────────┘ │
└─────────────────────────────────────────────┘
```
**步骤:**
1. 读取录制脚本文件内容
2. 在页面导航前通过 CDP 注入
```cpp
// C++ 侧伪代码SuperRPA Browser Process
std::string record_script = ReadFile("record.umd.cjs");
std::string injection_code = record_script + R"(
(function() {
window.__rrweb_events = [];
window.__rrweb_stop = rrwebRecord.record({
emit: function(event) {
// 方式1存到全局变量由 CDP 定时读取
window.__rrweb_events.push(event);
// 方式2通过 postMessage 发给 content script
window.postMessage({
type: '__rrweb_event',
event: event
}, '*');
}
});
// 通知控制层录制已就绪
window.postMessage({ type: '__rrweb_ready' }, '*');
})();
)";
// 通过 CDP 注入
cdp_session->Send("Page.addScriptToEvaluateOnNewDocument", {
{"source", injection_code}
});
```
```python
# Python 侧伪代码(如果用 Puppeteer/Playwright 驱动)
import json
cdp = await page.context().new_cdp_session(page)
await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
"source": record_script + """
window.__rrweb_events = [];
window.__rrweb_stop = rrwebRecord.record({
emit(event) {
window.__rrweb_events.push(event);
}
});
"""
})
# 导航到目标页面
await page.goto("https://example.com")
# 定时采集事件
events = await page.evaluate("window.__rrweb_events.splice(0)")
# 停止录制
await page.evaluate("window.__rrweb_stop()")
```
### 方案 BChromium 源码层注入(改动较大,性能最优)
在 Chromium 的 `ContentRendererClient::RenderFrameCreated``RenderFrameImpl::DidCreateDocumentElement` 中直接注入。
```cpp
// content/renderer/render_frame_impl.cc 或对应的 SuperRPA 扩展点
void SuperRPARenderFrameObserver::DidCreateDocumentElement() {
auto& web_frame = render_frame_->GetWebFrame();
std::string script = Load rrweb_record_script + R"(
window.__rrweb_events = [];
window.__rrweb_stop = rrwebRecord.record({
emit(event) {
window.__rrweb_events.push(event);
}
});
)";
web_frame.ExecuteScript(
WebScriptSource(WebString::FromUTF8(script)));
}
```
### 方案 CChrome Extension 方式
rrweb 项目已包含一个浏览器扩展示例(`packages/web-extension/`),可直接参考或改造。
---
## 4. 核心 API
### 4.1 录制
```js
// 开始录制
const stopFn = rrwebRecord.record({
emit(event) {
// event: eventWithTime 类型,每次用户操作或 DOM 变更触发
// 必须保证所有 event 按 timestamp 顺序存储
sendToBackend(event);
},
});
// 停止录制
stopFn();
```
### 4.2 回放
```js
// 创建回放器
const replayer = new rrweb.Replayer(events, {
root: document.getElementById('container'), // 回放容器 DOM
speed: 1,
});
// 获取元数据
const meta = replayer.getMetaData();
// meta = { startTime: number, endTime: number, totalTime: number }
// 播放控制
replayer.play(); // 从头播放
replayer.play(5000); // 从 5000ms 处播放
replayer.pause(); // 暂停在当前位置
replayer.pause(3000); // 暂停在 3000ms 处
// 配置变更
replayer.setConfig({ speed: 2 }); // 修改播放速度
// 销毁
replayer.destroy();
```
### 4.3 事件监听
```js
replayer.on('finish', () => { /* 回放结束 */ });
replayer.on('resize', (dimension) => { /* 视口大小变化 */ });
```
---
## 5. 事件数据结构
### 5.1 顶层结构
每个事件的顶层结构为 `eventWithTime`
```ts
{
timestamp: number; // Unix 时间戳(毫秒),相对于页面加载的偏移量
type: EventType; // 事件类型(数值枚举)
data: object; // 事件数据,结构因 type 不同而异
}
```
### 5.2 EventType 枚举
```ts
enum EventType {
DomContentLoaded = 0, // DOM 加载完成
Load = 1, // 页面完全加载
FullSnapshot = 2, // 全量 DOM 快照
IncrementalSnapshot = 3, // 增量快照(最重要的类型)
Meta = 4, // 页面元信息URL、视口尺寸
Custom = 5, // 自定义事件
Plugin = 6, // 插件事件
}
```
### 5.3 一个完整的录制会话包含的事件序列
```
1. Meta → { href: "页面URL", width: 1920, height: 1080 }
2. FullSnapshot → 完整序列化的 DOM 树
3. IncrementalSnapshot × N → 各种增量变更事件
...可选DomContentLoaded / Load
```
**关键点**:每个会话必须以一个 `Meta` 事件 + 一个 `FullSnapshot` 事件开头,后续才有意义。
### 5.4 IncrementalSnapshot 的 data.source 枚举
```ts
enum IncrementalSource {
Mutation = 0, // DOM 变更(增删改节点/属性/文本)
MouseMove = 1, // 鼠标移动
MouseInteraction = 2, // 鼠标交互(点击、双击、右键、聚焦等)
Scroll = 3, // 滚动
ViewportResize = 4, // 视口尺寸变化
Input = 5, // 输入框值变化
TouchMove = 6, // 触摸移动
MediaInteraction = 7, // 媒体播放/暂停/跳转
StyleSheetRule = 8, // CSS 规则变更
CanvasMutation = 9, // Canvas 绑定调用
Font = 10, // 字体加载
Log = 11, // 控制台日志(需插件)
Drag = 12, // 拖拽
StyleDeclaration = 13, // 行内样式声明变更
Selection = 14, // 文本选区变化
AdoptedStyleSheet = 15, // Adopted Stylesheet
CustomElement = 16, // 自定义元素注册
}
```
### 5.5 常见事件数据示例
**Meta 事件**
```json
{
"type": 4,
"data": { "href": "https://example.com/page", "width": 1920, "height": 1080 },
"timestamp": 1712600000000
}
```
**全量快照**
```json
{
"type": 2,
"data": {
"node": {
"type": 0,
"id": 1,
"childNodes": [
{ "type": 2, "tagName": "html", "id": 2, "attributes": { "lang": "zh" }, "childNodes": [...] }
]
},
"initialOffset": { "top": 0, "left": 0 }
},
"timestamp": 1712600000001
}
```
**鼠标点击**
```json
{
"type": 3,
"data": {
"source": 2,
"type": 2,
"id": 42,
"x": 320,
"y": 150
},
"timestamp": 1712600002500
}
```
**输入框变化**
```json
{
"type": 3,
"data": {
"source": 5,
"id": 15,
"text": "用户输入的内容",
"isChecked": false
},
"timestamp": 1712600003000
}
```
**DOM 变更**
```json
{
"type": 3,
"data": {
"source": 0,
"texts": [],
"attributes": [{ "id": 7, "attributes": { "class": "active" } }],
"removes": [],
"adds": [
{
"parentId": 5,
"nextId": null,
"node": { "type": 2, "tagName": "div", "id": 99, "attributes": {}, "childNodes": [] }
}
]
},
"timestamp": 1712600004000
}
```
**滚动**
```json
{
"type": 3,
"data": { "source": 3, "id": 1, "x": 0, "y": 500 },
"timestamp": 1712600005000
}
```
### 5.6 序列化节点类型
全量快照和增量快照中的新增节点使用以下类型:
```ts
enum NodeType {
Document = 0, // 文档节点
DocumentType = 1, // <!DOCTYPE ...>
Element = 2, // HTML 元素
Text = 3, // 文本节点
CDATA = 4, // CDATA 节点
Comment = 5, // 注释节点
}
```
每个序列化节点都有唯一的 `id`(自增数字),用于增量快照中引用和定位。
---
## 6. 录制配置详解
```js
rrwebRecord.record({
// 必填:事件回调
emit(event) {
// 每个 event 都必须保存,不可丢弃
// events 必须按 timestamp 有序
},
// ---- 隐私保护 ----
// 遮挡所有输入框内容(记录为 *
maskAllInputs: false,
// 细粒度控制哪些输入类型被遮挡
maskInputOptions: {
password: true,
text: false,
email: false,
// 支持所有 input type
},
// 自定义遮挡函数
maskInputFn: (text) => text.replace(/./g, '*'),
// 通过 CSS 选择器遮挡元素内的文本
maskTextSelector: '.sensitive-data',
// 自定义文本遮挡函数
maskTextFn: (text) => '***',
// CSS 类名方式遮挡文本(默认 'rr-mask'
maskTextClass: 'rr-mask',
// ---- 屏蔽/忽略 ----
// 不录制该 CSS 类名的元素及其子树(显示为灰色色块)
blockClass: 'rr-block',
blockSelector: '.no-record-area',
// 完全忽略该元素(不录制,回放时也不出现)
ignoreClass: 'rr-ignore',
ignoreSelector: '.ignore-me',
// ---- 采样策略(影响性能和存储体积的关键配置)----
sampling: {
// 鼠标移动false=不录制,数字=节流间隔(ms)
mousemove: 50,
// 鼠标交互false=不录制true=全部录制,或细粒度控制
mouseInteraction: {
MouseUp: true,
MouseDown: true,
Click: true,
ContextMenu: false,
DblClick: true,
Focus: true,
Blur: true,
TouchStart: true,
TouchEnd: true,
},
// 滚动节流间隔(ms)
scroll: 150,
// 媒体交互节流间隔(ms)
media: 800,
// 输入事件:'all'=记录每次按键,'last'=只记录最终值
input: 'last',
// Canvas'all'=记录每次API调用数字=每秒截图次数(1-60)
canvas: 'all',
},
// ---- 高级功能 ----
recordCanvas: false, // 是否录制 Canvas
recordCrossOriginIframes: false, // 是否录制跨域 iframe
recordAfter: 'DOMContentLoaded', // 何时开始录制: 'DOMContentLoaded' | 'load'
inlineStylesheet: true, // 是否内联外部样式表
inlineImages: false, // 是否内联图片为 base64
collectFonts: false, // 是否收集动态加载的字体
// 定期生成全量快照(防止增量快照链过长导致回放失败)
checkoutEveryNth: 100, // 每 N 个增量事件生成一次
checkoutEveryNms: 60000, // 每 N 毫秒生成一次
// 数据压缩(需配合 @rrweb/packer
// packFn: pack,
// 错误处理
errorHandler: (error) => {
console.error('rrweb error:', error);
// return true 可跳过该错误继续录制
},
// 插件
plugins: [
// console 录制插件
// getRecordConsolePlugin(),
],
});
```
---
## 7. SuperRPA 集成注意事项
### 7.1 注入时机
录制脚本必须在页面的 DOM 构建之前或 `DOMContentLoaded` 之前注入。使用 `Page.addScriptToEvaluateOnNewDocument` 可以保证在页面任何脚本执行前注入。
如果注入太晚,会丢失页面初始状态的快照。
### 7.2 事件采集
在 SuperRPA 中,需要将录制事件从渲染进程传递到控制层。推荐方式:
| 方式 | 适用场景 | 注意事项 |
|------|----------|----------|
| `Runtime.evaluate` 定时轮询 `window.__rrweb_events` | 简单场景 | 高频轮询有性能开销 |
| CDP `Runtime.binding` | 高性能 | 需要在 CDP session 上创建 binding |
| `window.postMessage` + Content Script | 扩展模式 | 需要 content script 中转 |
| V8 Inspector 自定义消息 | 深度集成 | 改动较大 |
推荐 **CDP `Runtime.binding`** 方案:
```js
// 注入脚本
window.__rrweb_events = [];
rrwebRecord.record({
emit(event) {
// 通过 CDP binding 发送
if (typeof window.__rrwebSend === 'function') {
window.__rrwebSend(JSON.stringify(event));
}
// 同时存入数组作为备份
window.__rrweb_events.push(event);
}
});
```
```python
# 控制端
await cdp.send("Runtime.addBinding", {"name": "__rrwebSend"})
cdp.on("Runtime.bindingCalled", lambda params: handle_event(
json.loads(params["payload"])
))
```
### 7.3 会话管理
一个「录制会话」对应一次页面导航navigation。需要注意
1. **单页应用 (SPA)**页面内路由切换不会触发新的导航rrweb 会持续录制整个 SPA 会话
2. **多页导航**:每次导航需要重新注入录制脚本并开始新会话
3. **跨域 iframe**:每个跨域 iframe 也需要单独注入录制脚本rrweb 会通过 `postMessage` 桥接
4. **事件合并**:一个完整会话的所有事件必须按 `timestamp` 有序存储
### 7.4 性能影响
| 配置 | 性能影响 | 存储体积 |
|------|----------|----------|
| 默认配置 | 中等 | 中等 |
| `sampling.mousemove = false` | 低 | 小 |
| `sampling.input = 'last'` | 低 | 小 |
| `recordCanvas = true` | 高 | 大 |
| `recordCanvas = true` + `sampling.canvas = 10` | 中 | 中 |
| `inlineStylesheet = true` | 中 | 大(取决于页面样式复杂度) |
| `checkoutEveryNms = 30000` | 略高 | 略大(但提高回放鲁棒性) |
**建议基线配置**
```js
{
sampling: {
mousemove: 50,
scroll: 150,
input: 'last',
media: 800,
},
recordCanvas: false,
checkoutEveryNms: 300000, // 每 5 分钟一次全量快照
}
```
### 7.5 存储优化
1. **使用 packFn 压缩**
```js
import { pack } from '@rrweb/packer';
record({ emit(event) {}, packFn: pack });
```
2. **服务端批量压缩**:采集原始事件,服务端用 gzip/zstd 压缩效果更好
3. **定期全量快照**:通过 `checkoutEveryNms` 避免增量链过长
4. **跨会话去重**:相同样式表、相同页面的全量快照可以做去重
### 7.6 回放端集成
回放可以在 SuperRPA 内的专用标签页中展示,或在外部系统中:
```js
// 在空白页面中创建回放器
const replayer = new rrweb.Replayer(events, {
root: document.getElementById('replay-container'),
speed: 1,
skipInactive: true, // 跳过无操作的静默期
inactivePeriodThreshold: 5000, // 超过 5 秒无操作则跳过
showWarning: true,
insertStyleRules: [ // 注入自定义样式(如鼠标光标样式)
'div.replayer-mouse { cursor: pointer; }'
],
});
// 监听事件
replayer.on('finish', () => console.log('回放结束'));
replayer.on('resize', (d) => console.log('视口变化', d));
// 播放控制
replayer.play();
replayer.pause();
replayer.setConfig({ speed: 4 });
```
回放需要加载 CSS 样式:
```html
<link rel="stylesheet" href="replay/dist/style.css">
```
---
## 8. 录制事件中提取 RPA 可用信息
如果 SuperRPA 不仅需要视觉回放,还需要从事件中提取 **可复用的操作信息**(如元素定位、操作类型),可以从以下字段获取:
### 8.1 鼠标点击事件 → 点击坐标 + 目标元素
```json
{
"type": 3,
"data": {
"source": 2,
"type": 2, // MouseInteractions.Click = 2
"id": 42, // 序列化节点 ID
"x": 320, "y": 150 // 相对于视口的坐标
}
}
```
- `id` 对应全量快照中的节点 ID可反查到 `tagName``attributes`(含 `class``id` 等)
- `x``y` 为点击坐标
### 8.2 输入事件 → 输入值 + 目标元素
```json
{
"type": 3,
"data": {
"source": 5,
"id": 15,
"text": "用户输入",
"isChecked": false
}
}
```
### 8.3 滚动事件 → 滚动位置
```json
{
"type": 3,
"data": { "source": 3, "id": 1, "x": 0, "y": 500 }
}
```
### 8.4 从全量快照反查元素定位信息
全量快照中包含完整的 DOM 树,每个节点有 `id``tagName``attributes`。可以通过 ID 反查来构造 CSS 选择器或 XPath。
```js
// 从全量快照中构建 ID → 节点映射
function buildNodeMap(fullSnapshot) {
const map = {};
function walk(node) {
map[node.id] = node;
if (node.childNodes) node.childNodes.forEach(walk);
}
walk(fullSnapshot.data.node);
return map;
}
```
### 8.5 MouseInteractions 枚举
```ts
enum MouseInteractions {
MouseUp = 0,
MouseDown = 1,
Click = 2,
ContextMenu = 3,
DblClick = 4,
Focus = 5,
Blur = 6,
TouchStart = 7,
TouchMove_Departed = 8,
TouchEnd = 9,
TouchCancel = 10,
}
```
---
## 9. 完整注入示例
以下是一个可直接使用的注入脚本模板:
```js
// === rrweb-recorder-inject.js ===
// 此文件应包含 record.umd.cjs 的完整内容
// 然后附加以下启动代码
(function() {
'use strict';
var SESSION_ID = '__SESSION_ID_PLACEHOLDER__'; // 由控制层替换
var events = [];
function sendEvent(event) {
// 使用 CDP binding如果可用
if (typeof window.__rrwebSendEvent === 'function') {
try {
window.__rrwebSendEvent(JSON.stringify({
sessionId: SESSION_ID,
event: event
}));
} catch (e) {
// fallback
events.push(event);
}
} else {
events.push(event);
}
}
// 开始录制
var stopFn = rrwebRecord.record({
emit: sendEvent,
maskAllInputs: false,
recordCanvas: false,
recordCrossOriginIframes: true,
recordAfter: 'DOMContentLoaded',
sampling: {
mousemove: 50,
scroll: 150,
input: 'last',
media: 800,
},
checkoutEveryNms: 300000,
});
// 暴露控制接口
window.__rrweb = {
stop: function() {
if (stopFn) stopFn();
},
getEvents: function() {
return events;
},
drainEvents: function() {
var result = events.slice();
events.length = 0;
return result;
},
getSessionId: function() {
return SESSION_ID;
}
};
// 通知就绪
if (typeof window.__rrwebReady === 'function') {
window.__rrwebReady(SESSION_ID);
}
})();
```
---
## 10. 故障排查
| 问题 | 可能原因 | 解决方案 |
|------|----------|----------|
| 回放白屏 | 缺少 Meta + FullSnapshot 事件 | 确保从页面加载开始就注入了录制脚本 |
| 回放缺少样式 | `inlineStylesheet` 未开启或外部样式表加载失败 | 设置 `inlineStylesheet: true` |
| 回放中元素错位 | 视口尺寸与录制时不一致 | 使用 Meta 事件中的 width/height 创建同尺寸回放容器 |
| 录制数据过大 | Canvas 动画或频繁 DOM 更新 | 配置 `sampling``blockClass`,或关闭 `recordCanvas` |
| 跨域 iframe 无录制 | 未对 iframe 注入脚本 | 开启 `recordCrossOriginIframes: true` 并确保 iframe 中也注入了脚本 |
| 回放报错 "node not found" | 增量快照链中丢失了事件 | 确保所有事件都被保存,使用 `checkoutEveryNms` 定期全量快照 |
| 注入后页面行为异常 | rrweb 脚本与页面脚本冲突 | 使用 `ignoreSelector` 排除 rrweb 自身的 UI 元素 |
---
## 11. 参考链接
- rrweb 官方文档:`docs/` 目录下
- 事件数据结构定义:`packages/types/src/index.ts`
- 录制逻辑:`packages/rrweb/src/record/``packages/record/src/`
- 回放逻辑:`packages/rrweb/src/replay/``packages/replay/src/`
- 浏览器扩展示例:`packages/web-extension/`
- DOM 序列化设计:`docs/serialization.md`
- 增量快照设计:`docs/observer.md`
- 回放沙箱设计:`docs/sandbox.md`