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

22 KiB
Raw Permalink Blame History

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 构建

# 在 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/replayrrweb-player

2.3 UMD 全局变量名

注入后rrweb 的 API 会挂载到全局变量上:

  • rrweb.umd.cjswindow.rrweb(包含 recordReplayer
  • record.umd.cjswindow.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 注入
// 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 侧伪代码(如果用 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::RenderFrameCreatedRenderFrameImpl::DidCreateDocumentElement 中直接注入。

// 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 录制

// 开始录制
const stopFn = rrwebRecord.record({
  emit(event) {
    // event: eventWithTime 类型,每次用户操作或 DOM 变更触发
    // 必须保证所有 event 按 timestamp 顺序存储
    sendToBackend(event);
  },
});

// 停止录制
stopFn();

4.2 回放

// 创建回放器
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 事件监听

replayer.on('finish', () => { /* 回放结束 */ });
replayer.on('resize', (dimension) => { /* 视口大小变化 */ });

5. 事件数据结构

5.1 顶层结构

每个事件的顶层结构为 eventWithTime

{
  timestamp: number;   // Unix 时间戳(毫秒),相对于页面加载的偏移量
  type: EventType;     // 事件类型(数值枚举)
  data: object;        // 事件数据,结构因 type 不同而异
}

5.2 EventType 枚举

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 枚举

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 事件

{
  "type": 4,
  "data": { "href": "https://example.com/page", "width": 1920, "height": 1080 },
  "timestamp": 1712600000000
}

全量快照

{
  "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
}

鼠标点击

{
  "type": 3,
  "data": {
    "source": 2,
    "type": 2,
    "id": 42,
    "x": 320,
    "y": 150
  },
  "timestamp": 1712600002500
}

输入框变化

{
  "type": 3,
  "data": {
    "source": 5,
    "id": 15,
    "text": "用户输入的内容",
    "isChecked": false
  },
  "timestamp": 1712600003000
}

DOM 变更

{
  "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
}

滚动

{
  "type": 3,
  "data": { "source": 3, "id": 1, "x": 0, "y": 500 },
  "timestamp": 1712600005000
}

5.6 序列化节点类型

全量快照和增量快照中的新增节点使用以下类型:

enum NodeType {
  Document = 0,      // 文档节点
  DocumentType = 1,  // <!DOCTYPE ...>
  Element = 2,       // HTML 元素
  Text = 3,          // 文本节点
  CDATA = 4,         // CDATA 节点
  Comment = 5,       // 注释节点
}

每个序列化节点都有唯一的 id(自增数字),用于增量快照中引用和定位。


6. 录制配置详解

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 方案:

// 注入脚本
window.__rrweb_events = [];
rrwebRecord.record({
  emit(event) {
    // 通过 CDP binding 发送
    if (typeof window.__rrwebSend === 'function') {
      window.__rrwebSend(JSON.stringify(event));
    }
    // 同时存入数组作为备份
    window.__rrweb_events.push(event);
  }
});
# 控制端
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 略高 略大(但提高回放鲁棒性)

建议基线配置

{
  sampling: {
    mousemove: 50,
    scroll: 150,
    input: 'last',
    media: 800,
  },
  recordCanvas: false,
  checkoutEveryNms: 300000,  // 每 5 分钟一次全量快照
}

7.5 存储优化

  1. 使用 packFn 压缩
import { pack } from '@rrweb/packer';
record({ emit(event) {}, packFn: pack });
  1. 服务端批量压缩:采集原始事件,服务端用 gzip/zstd 压缩效果更好
  2. 定期全量快照:通过 checkoutEveryNms 避免增量链过长
  3. 跨会话去重:相同样式表、相同页面的全量快照可以做去重

7.6 回放端集成

回放可以在 SuperRPA 内的专用标签页中展示,或在外部系统中:

// 在空白页面中创建回放器
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 样式:

<link rel="stylesheet" href="replay/dist/style.css">

8. 录制事件中提取 RPA 可用信息

如果 SuperRPA 不仅需要视觉回放,还需要从事件中提取 可复用的操作信息(如元素定位、操作类型),可以从以下字段获取:

8.1 鼠标点击事件 → 点击坐标 + 目标元素

{
  "type": 3,
  "data": {
    "source": 2,
    "type": 2,         // MouseInteractions.Click = 2
    "id": 42,          // 序列化节点 ID
    "x": 320, "y": 150 // 相对于视口的坐标
  }
}
  • id 对应全量快照中的节点 ID可反查到 tagNameattributes(含 classid 等)
  • xy 为点击坐标

8.2 输入事件 → 输入值 + 目标元素

{
  "type": 3,
  "data": {
    "source": 5,
    "id": 15,
    "text": "用户输入",
    "isChecked": false
  }
}

8.3 滚动事件 → 滚动位置

{
  "type": 3,
  "data": { "source": 3, "id": 1, "x": 0, "y": 500 }
}

8.4 从全量快照反查元素定位信息

全量快照中包含完整的 DOM 树,每个节点有 idtagNameattributes。可以通过 ID 反查来构造 CSS 选择器或 XPath。

// 从全量快照中构建 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 枚举

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. 完整注入示例

以下是一个可直接使用的注入脚本模板:

// === 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 更新 配置 samplingblockClass,或关闭 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