最近在做 web 端录制 和回放的时候遇到一个比较好的库 rrweb, 但是网上的资料相对比较少,撸了遍源码,做个小总结。本文阅读大概会了解到以下几个方面~
-
rrweb 是什么
-
适用场景
-
录制回放实现原理
-
使用 rrweb 应注意的问题
- 数据量大的问题
-
安全问题
-
rrweb 库组成部分 + 简介
-
rrweb 各个模块对应的技术细节
一、rrweb 是什么?
web 端录制回放的一个基础库,即记录页面中的 DOM 结构还有用户操作行为,在远程实现回放。一图胜千言,可看下面的gif 图

二、适用场景
- 记录⽤户使⽤产品的⽅式并加以分析,进⼀步优化产品。
- 采集⽤户遇到 bug 的操作路径,予以复现。
- 记录 CI 环境中的 E2E 测试的执⾏情况。
- 录制体积更⼩、清晰度⽆损的产品演⽰。
三、录制回放基本原理
这一块可以简单地理解为 “快照 + 操作指令”, 一般记录页面状态,我们可以根据时间记录每一时刻页面的 DOM状态,回放的时候根据时间点显示即可,但是一般一个页面DOM 数据是很庞大的,如下图所示,这个还是页面 DOM 比较少的情况

因此 rrweb 采取了另外一种方式,记录初始页面的DOM 状态,或者特定某个时刻的DOM 状态,后续收集的是不同时间点的操作指令 或者 某个时刻 某个DOM 的变化作为一个增量快照,在原先快照的基础上,不断加入根据行为解析的DOM 数据,构建了后续的快照,减少大量数据的存储或传输。

四、使用 rrweb 应注意的问题
- 数据量大的问题
- 数据安全问题 关于数据大的问题,其实rrweb 内部有做了一些处理,比如:
- 根据DOM 变化或者有特定的用户操作行为时才收集数据;
- 数据切片,分片不是单个数据的分片,而是如何把一天的数据或者是连续不刷新页面的数据进行分片,在rrweb里叫snapshot,比如每隔10分钟或者数据超过一定大小之后,进行一次数据分片,可以分开存储,这样在播放的时候,可以获取某一段的数据,不用播放一整天的。
- 节流,对于鼠标移动,页面滚动事件进行了节流
除此以外,可自己对收集到的数据进行处理
- 压缩数据,尝试过用 pako.js 进行压缩
- 将数据切片保存在不同的文件存储在云服务器,在播放的时候拉取文件整合数据再播放(这里只是一个想法,还没验证是否可行)
关于数据安全问题,这个应该加密即可
五、rrweb 组成部分

六、rrweb 各个模块对应的技术细节
1、rrweb-snapshot 提供了 snapshot, resbuid接口,snapshot遍历 页面DOM 返回当前页面 DOM 视图的一个序列化的数据结构, rebuild, 则解析特定的数据还原DOM, 并插入文档中 例如:

这个模块的功能主要有: a) snapshot 方法
- 为每个节点提供一个id,并在快照完成时返回id的节点映射
- 相对路径的处理,将href,src,CSS中的相对路径设为绝对路径
- 将页面引用的样式变为内联样式,以确保可以使用本地样式
- 将一些DOM状态内联到HTML属性中,例如HTMLInputElement的值
- 将script标记转换为noscript标记,以避免脚本被执行。
第一点:Snapshot 通过 takeFullSnapShotg构建页面 DOM 树,同时生成了 id -> Node 的映射,即在构建 DOM 树时为每个节点生成一个唯一的id, 同时根据 id 生成一份映射,这个映射只要是为了方便后续的增量快照操作



第二点: 将href,src,CSS中的相对路径设为绝对路径 将一些脚本,样式,图片等引用的相对路径改为绝对路径 比如:

那经过转换之后,回放时,图片的链接地址已经变为之前域名下的地址

第三点: 将页面引用的样式变为内联样式,以确保可以使用本地样式 将页面引用的样式读取变为内联样,例如


除此以外,还有一种样式逻辑,即通过 CssStyleSheet.sheet.insertRule的形式插入页面的样式:


第四点:将一些DOM状态内联到HTML属性中,例如HTMLInputElement的值
记录没有反映在 HTML 中的视图状态。例如 输⼊后的值不会反映在其 HTML中,我们需要读取其 value 值并加以记录

第五点:将script标记转换为noscript标记,以避免脚本被执行 在播放录制页面时,页面的脚本是不能够被执行的,需要禁掉
b) rebuild方法 通过创建Dom, 设置属性等,并且将对应的DOM 插入文档中

2、Rrweb 提供了 record 和 replay 功能。 a) record 方法: 前面说到增量快照,那增量数据是怎么收集的呢?开始录制之后,会针对当前页面生成一个DOM 快照,然后开始监听用户操作和页面DOM的变化。 监听行为如下:
- DOM 变动
- 节点创建、销毁
- 节点属性变化
- 文本变化
- 鼠标移动
- 鼠标交互
- mouse up、mouse down
- click、double click、context menu
- focus、blur
- touch start、touch move、touch end
- 页面或元素滚动
- 视窗大小改变
- 输入

监听的方式有: (1)MutationObserver 其中,监听 DOM 变化 主要是通过 API --- MutationObserver 来实现。当监视的DOM 发生变动时, MutationObserver 将收到通知并触发预先设定好的回调参数,与 addEventListener 方法 比较相似 例如:

如上图所示,当我们尝试改变页面 DOM 的属性,或者新增 DOM 节点的时候,都会对应生成一条 MutationObserver record, record 记录了一些变动信息~ 在 rrweb 中, 对每一条mutation record 做了几下处理

针对不同的类型进行处理, characterData 是节点内容或节点文本变动,attributes 是节点属性的变动,childList 是子节点的变动,包括新增子节点,移除子节点,移动子节点等。
新增节点的逻辑 在初始记录时,生成了页面快照同时维护一个 id -> Node 的映射, 因此当出现新增节点时,无需重新完整地生成一份快照,而只需要将新节点序列化并加入映射中即可。 由于MutationObserver触发方式为批量异步回调,具体来说就是会在一系列 DOM 变化发生之后将这些变化一次性回调,传出的是一个 mutation 记录数组,那在序列化的时候,会存在重复记录的问题 例如以下例子中

以下两种方式都可以生成这种DOM 结构
1、创建节点 n1 并 append 在 body 中,再创建节点 n2 并 append 在 n1 中。 2、创建节点 n1、n2,将 n2 append 在 n1 中,再将 n1 append 在 body 中。
MutationObserver 对这两种Dom 操作方式的输出记如下图所示
第一种方式 record 记录

这种情况下虽然 n1 append 时还没有子节点,但是由于上述的批量异步回调机制,当我们处理 mutation 记录时获取到的 n1 是已经有子节点 n2 的状态
第二种方式 record 记录

在处理序列化的过程中,在处理新增节点时必须遍历其所有子孙节点,才能保证所有新增节点都被记录,但是这一策略应用在第一种情况中就会导致 n2 被作为新增节点记录两次,回放时就会产生与原页面不一致的 DOM 结构,因此,为避免这种情况, rrweb 中采取了‘惰性’新增节点,即在遍历 mutation record 时,不会立马去序列化新增的节点,而是收集新增的节点,在遍历完mutation 所有记录的时候再统一去重,序列化新增节点。

删除节点的逻辑

(2)鼠标移动,鼠标交互,页面滚动,视窗大小这些则通过 事件绑定的形式去监听,如下面页面滚动的监听


on 方法也是通过 addEventListener 的形式监听
b) replay 解析收集到的events 集合,进行还原 收集到的事件类型有
- DomContentLoaded
- Load
- FullSnapshot
- IncrementalSnapshot
- Meta 其中 当事件类型为 FullSnapshot时,会调用rebuild, 根据快照数据生成页面的DOM, 当事件类型为 IncrementalSnapshot 时,则说明是增量快照,即收集的数据只是DOM 的变化数据或者对应的用户行为数据,根据不同的数据类型做对应的节点插入,删除,节点属性的更改等

七、写在最后
第一次阅读源码,写分析,可能比较粗糙,若有错误之处,欢迎指正。也欢迎一起交流,相关的问题也可以到github 上向作者提问哦,作者响应问题速度也是挺快的。