在我长期的固有认知中:为了收集和上报网站交互过程中 JavaScript 的报错信息和其它相关数据,我们一般会采用自研或者第三方的SDK,也可以简单理解为埋点。这也是为了方便生产问题的排查,做到可溯源。
但是,在前一段,和隔壁组的同事交流时,我发现了一个有点东西的系统--Rejouer。这个系统可以做到完整复现用户的操作行为,类似于录屏的功能,起初我还真的以为他们就是给我放了一个录屏,后来发现没那么简单。
本文也是在和这位同事探讨的过程中得到的一些启发。
业务背景
不要重复造轮子!这是我一直以来的信念。轮子的出现必定是要为业务服务。那么Rejouer出现的背景是什么呢?
痛点
我们来回顾一下日常处理线上问题的场景:
上面图片中的是一个正常处理线上问题的流程。我们一般会通过之前埋入的一些业务打点,根据用户uid去追溯问题,这样基本可以解决大部分的线上问题。但在实际的业务场景中,总是会出现一些奇奇怪怪的问题:
环境差异:客户版本和配置差异内容传递不完整:在上图第 2 步中,出现信息传递不对称,造成问题无法复现敏感性:上图第 4 步中出现本地无法复现,咨询客户,但又涉及较为私密的信息,用户不愿录屏复现的场景客户数据差异:即使用户愿意录屏,但可能由于服务抖动,用户此时再次走流程也无法复现- ...
除了上面提到的这些问题,还有一个影响线上问题解决速度最致命的:链路过长!!🤐
由此可见定位问题的痛点也是足够多。
期望的结果
上面提到了那么多的业务痛点,那么为了解决这些问题,我们肯定期望能有这么一个系统:
技术调研
有了上面的需求,下面让我们来做下技术调研,看如何进行实现。
初期的调研,探寻了 5 种可行的方案:
我们一一来进行分析。
webRTC
首先简单了解一下webRTC:它是一套客户端点对点流式信息传播的技术方案。
其不仅可以用于「音视频录制、视频通话」,还可以用在「照相机、音乐播放器、共享远程桌面、即时通信工具、P2P 网络加速、文件传输、实时人脸识别」等场景上。
但我们来看下它的兼容性情况:
不支持 IE,这个就有点不太友好了,而且是完全不支持(毕竟我们还是有部分的用户在使用IE浏览器的😂)
除了兼容性之外,还有另外一个问题:webrtc在启动能力前,会有原生的chrome窗口弹出来询问用户,且在开启getDisplayMedia之后会有一个状态条,所以在整个的操作中会有一些生硬,用户有明显的知道自己的屏幕正在被录制。
显然针对这种场景,更适合用在测试场景下,或者明确授权情况下。也不符合我们的需求。
Puppeteer
puppeteer 是谷歌官方出品的一个通过 DevTools 协议控制 headless Chrome 的 Node 库,我们可以通过 puppeteer 提供的 API 直接控制 Chrome,进而模拟大部分用户在浏览器上的操作,来进行 UI Test 或者作为爬虫访问页面来收集数据。
但puppeteer主要的问题是它强依赖于Chromium,客户操作也较为复杂。
因此也不是太符合我们的设定。
浏览器插件
简单来说:浏览器插件就是一个用Web技术开发、用来增强浏览器功能的软件。
正如其名,由于浏览器厂商多样性,其开发适配成本也不低。同时还伴随着诸如权限、培训使用等问题。
也不符合我们的需求。
html2Canvas
利用Canvas截图,使用 html2Canvas 库,不停的画页面然后不停的截图,再将图片组成视频播放出来。
这种也是我首先想到的方案,不过在脑海里没有停留多久,就被否决了:性能太差了!!
而且还存在诸如图片跨域兼容性问题。
好家伙,找了这么久,一个能用的都没有。把我都给整不会了 😷
冷静的分析一波:其实还有一个 API:MutationObserver。该接口提供了监视对 DOM 树所做更改的能力。我们可以利用这个接口,保存每次变化的DOM数据,并把这些数据转换成可视化的数据结构,然后分别保存起来。接着使用特定的方式对之前保存起来的DOM数据进行还原并重新渲染出来。DOM节点的变化也就意味了页面轨迹发送了变化。这样就可以把这些轨迹记录下来。
理想很丰满,现实真的挺残酷!🐶
真正自己去落地时还是遇到很多问题的,包括其中的很多细节,那么类似的社区里面有没有开源的解决方案呢?
你别说,还真有:rrweb
rrweb
打开rrweb官方地址:
诚如所言:rrweb 是一个开源的 Web 会话回放库,它提供易于使用的 API 来记录用户的交互并远程回放。
再看首页右侧的录制回放演示。
这不就是我们想要的吗?
体验了一波之后,我感觉rrweb基本能满足我们的业务诉求了:
关于
rrweb的基本使用,这里不会赘述,可参考rrweb 官方使用指南
rrweb内部做了什么?
回想我们起初做的一些调研,都没有成功落地。最终Rejouer的落地也是基于rrweb做了很多适配自身业务的封装。
本文后半部分更多是来探讨
rrweb,Rejouer由于是内部项目,更多细节暂时不便透露
对于rrweb内部的一些实现,我们还是比较好奇的。下面我准备深入去挖掘他的内部实现细节。
既然要分析它的内部运作流程,那么有几个前置的知识点需要提前同步一下:
- DOM 快照
- 定时快照
- 增量快照
- 引发视图变更
DOM 快照
⻚⾯中的视图状态可以通过 DOM 树的形式描述,所以当我们尝试录制⼀个⻚⾯时,我们实际上是在记录 DOM 树在各个时间点上的状态,在 rrweb 中我们称⼀次这样的状态记录为⼀个快照。
定时快照
定时快照的概念相对比较好理解:定时对⻚⾯制作快照完成录制。
但有很大的弊端:
- 性能损耗巨大
- 很多时候页面并没有发生变动,但也被记录下来了
所以产生了增量快照的概念。
增量快照
其实我们可以只在页面初始化完成之后 clone 一次完整的页面内容,等到页面有变动的时候,只记录变化的部分。这样一来,好处就显而易见了:
- 性能得到了很大的提升
- 网络开销也减少很多
- 避免了很多重复数据的收集
这里提到了只记录变化的部分,那这个变化又该怎么去衡量呢?
我们可以暂且把这种变化称为视图变更。
视图变更
引发视图变更的操作大致有:
- DOM 变动
- 节点创建、销毁
- 节点属性变化
- ⽂本变化
- ⿏标交互
- ⻚⾯或元素滚动
- 视窗⼤⼩改变
- 输⼊
- ⿏标移动(特指⿏标的视觉位置)
这里的DOM 变动我们可以采用MutationObserver来监听,它会以批量的方式返回 dom 的更新记录。
但是它没法跟踪像 input、textarea、select 这类可交互元素的输入。
对于这种可交互的元素,我们主要靠监听 input 和 change 来记录输入的过程。
但是有些元素的值是通过程序直接设置的,这样是不会触发 input 和 change 事件的。这种情况下我们可以通过劫持对应属性的 setter 来达到监听的目的。
ok,有了这些知识,下面我们就来看一下rrweb的运行流程吧。
运行流程
这里可以简单用一张图来概括:
在 DOM 加载完成后,record 会做一次完整的 DOM 序列化,我们把它叫做全量快照,全量快照记录了整个 HTML 数据结构。
然后在页面发生视图变动时(DOM 变化和用户操作),记录增量数据,完成页面的录制,然后保存到远程服务器。
回放时会先创建沙箱环境,接着重建全量快照,在通过 requestAnimationFrame 模拟定时器的方式来播放增量快照。
总结
本文主要整理了在后埋点时代针对业务中一些难复现、易引起客户纠纷的一些场景,在页面录制/回放道路上的探索过程。探讨了几种解决方案的利弊,最终也是基于社区非常优秀的rrweb做的封装。同时也分析了一下rrweb的核心实现。关于rrweb更多细节的探讨可参考rrweb:打开 web 页面录制与回放的黑盒子 。