rrweb可以进行屏幕录制和回放,用它做一个用户自助反馈功能,可以大幅度降低 bug
反馈成本。 这里简要介绍它的核心内部设计,后续会研究它的源码以增加一些定制功能。
rrweb
并不是真的在录制视频,而是将页面DOM
序列化后记录下来,再利用反序列化还原成DOM
来回放。
代码组成
主要分为 3 个部分(参照官方 README
):
rrweb-snapshot
: 包含 snapshot 和 rebuild 功能。snapshot 用于将 DOM 及其状态转化为可序列化的数据结构;rebuild 则是将 snapshot 记录的数据结构重建为对应的 DOM。rrweb
: 包含 record 和 replay 两个功能。record 用于记录 DOM 中的所有变更(mutation);replay 则是将记录的变更按照对应的时间一一重放。rrweb-player
:为 rrweb 提供一套 UI 控件,提供基于 GUI 的暂停、快进、拖拽至任意时间点播放等功能。
对于一个自助反馈功能来说,需要rrweb
+rrweb-snapshot
来生成一组序列化的snapshot
,并发送给回放后台;后台回放页面拿到snapshot
后,利用rrweb-snapshot
还原为DOM
,结合replay
功能后就可以在rrweb-player
中”播放”了。
内部设计
这个部分主要也是阅读了rrweb
官网的 4 篇文章后,进行的一个简要总结,原始链接如下:
序列化
并不需要将每时每刻的 DOM 都全量序列化,这样一来数据量很大,另外会有很多重复数据。rrweb 会在初始时进行一次全量序列化,然后将各种页面活动比如按钮点击转变为增量序列化。
全量序列化
将DOM
树转为”虚拟 DOM“树形数据结构。例如:
1 | <html> |
会被序列化为:
1 | { |
基于这样的树形结构后,假如要记录某个按钮的点击,对于这个操作就可以序列化为类似如下的结构:
1 | type clickSnapshot = { |
增量序列化
在完成一次全量快照之后,需要基于当前视图状态观察所有可能对视图造成改动的事件,目前在 rrweb 中已经观察了以下事件:
DOM
变动- 节点创建、销毁
- 节点属性变化
- 文本变化
- 鼠标移动
- 鼠标交互
mouse up
、mouse down
click
、double click
、context menu
focus
、blur
touch start
、touch move
、touch end
- 页面或元素滚动
- 视窗大小改变
input
输入
rrweb
基于MutationObserver
来观察所有这些变更,MutationObserver
的一个示范:
1 | // Select the node that will be observed for mutations |
新增节点
由于MutationObserver
在触发时,callback
收到的是一批操作记录,这个特性会影响rrweb
的序列化过程,比如
1 | body |
- 创建节点
n1
并append
在body
中,再创建节点n2
并append
在n1
中 - 创建节点
n1
、n2
,将n2
append
在n1
中,再将n1
append
在body
中。
这两种操作的最终结果是一致的,不过在增量序列化时,前者会产生两条记录,后者只有一条记录,rrweb
需要区分开来。最终采取的方案是:
在新增节点时,所有 mutation
记录都需要先收集,再新增节点去重并序列化之后再做处理。
节点属性变化
对于节点属性的变化,比如resize textarea
时宽高会发生多次变更,这会导致增量记录大大增加,经权衡只记录最终的值。
鼠标移动
与节点属性变化的处理类似,记录鼠标轨迹也需要尽量减少增量记录。有两层节流:
- 每
20 ms
最多记录一次鼠标坐标 - 每
500 ms
最多发送一次鼠标坐标集,主要是做的一个分段
input
输入变更
- 界面交互引起的,主要靠监听
input
和change
两个事件 js
代码设置引起的,比如设置input DOM
的value
属性,这种主要是利用Object.defineProperty
拦截DOM
属性的setter
,类似于Vue
中的响应式数据
沙盒
在拿”录屏“数据后的回放界面中,需要禁用被录制页面中的所有js
,同时还有很多其他交互,比如表单提交、window.open
打开新窗口、内联脚本等等。 所以rrweb
在回放系统中会将所有重建后的DOM
放到一个iframe
中,并设置相关sandbox 属性来禁用。
a
链接跳转也是需要禁止的,通过事件代理来preventDefault
掉所有a
链接的click
事件。
回放播放器
每个变更记录都带有时间戳,所以是可以做一个”播放器“来按时间顺序”播放“变更记录的。rrweb
利用requestAnimationFrame
来模拟随时间变化的回放。
从任意时间点开始播放:当在播放器中拖动到指定进度后,将进度之前的变更记录一次性同步执行掉,进度之后的再按照requestAnimationFrame
异步播放。
倍数:播放器还可以支持例如2
倍、4
倍数播放,这个也比较好做,相当于此前在一个raf
回调中执行一个变更记录,现在是执行2
个、4
个变更记录。