回放
rrweb 的一个设计原则是在录制侧尽可能少做处理,最大限度地降低对被录制页面的影响。这意味着我们需要在回放侧做一些特殊处理。
高精度计时器
回放时,我们会一次性拿到完整的快照链。如果按顺序执行所有快照,就能直接得到被录制页面的最终状态;但我们真正需要的是:同步初始化第一个全量快照,然后异步应用其余的增量快照。我们按照时间间隔一个接一个地回放每个增量快照,这就需要一个高精度计时器。
之所以强调高精度,是因为原生的 setTimeout 并不能保证在设定的延迟时间之后精确执行,例如当主线程被阻塞时。
对于我们的回放功能来说,这种不精确的延迟是不可接受的,会引发各种奇怪的现象,因此我们使用 requestAnimationFrame 实现了一个不断校准的计时器,以确保在大多数情况下增量快照的回放延迟不超过一帧。
同时,这个自定义计时器也是我们“快进”功能的基础。
补全缺失节点
增量快照设计中提到了 rrweb 使用 MutationObserver 时采用的延迟序列化策略,它可能导致以下无法录制完整增量快照的场景:
parent
node bar
node foo- 节点
foo被添加为 parent 的子节点 - 节点
bar被添加到已有子节点foo之前
按照实际执行顺序,foo 会先被 rrweb 序列化;但在序列化新节点时,除了父节点之外,我们还需要记录相邻节点,以确保回放时新添加的节点能被放到正确的位置。此时 bar 已经存在但尚未被序列化,因此我们会将它记录为 id: -1(或者,如果没有相邻节点,则以 null 作为 id,表示它不存在)。
回放时,当我们处理新增 foo 的增量快照时,由于其相邻节点的 id 为 -1,我们便知道该相邻节点还没有被插入,于是将 foo 暂时放入“缺失节点池”中,暂不插入 DOM 树。
在处理完新增 foo 的增量快照之后,我们会正常处理并插入 bar。回放完成后,我们会检查 foo 的相邻节点 id 是否指向缺失节点池中的某个节点;如果匹配,则将其从池中取出并插入 DOM 树。
模拟悬停
许多网页中都存在针对 :hover 选择器的 CSS 样式,但我们无法通过 JavaScript 触发悬停状态。因此在回放时,我们需要模拟悬停状态,使样式正确显示。
具体方法包括两部分:
- 遍历 CSS 样式表,为
:hover选择器添加与原来相同的 CSS 规则,但附加一个特殊的选择器类,例如.:hover。 - 在回放 mouse up 鼠标交互事件时,为事件目标及其所有祖先节点添加
.:hover类名,并在鼠标再次移开时将其移除。
从任意时间点开始播放
除了基本的回放功能之外,我们还希望 rrweb-player 这样的播放器能提供类似视频播放器的功能,例如拖拽进度条跳转到任意时间点。
在实际实现中,我们向方法传入一个起始时间。然后可以将快照链分为两部分:起始时间之前的部分和起始时间之后的部分。接着,同步执行起始时间之前的快照链,而起始时间之后的快照链则采用正常的异步执行。通过这种方式,我们就能实现从任意时间点开始回放。