基于rrweb框架对web 页面录制与回放

官方文档:https://github.com/rrweb-io/rrweb/blob/0f86a72705b998a9abf5b4aae5b01a4f3b679954/guide.zh_CN.md

前言

最近来了个需求需要对web 页面上的操作行为做流程跟踪,并提供具体的数据,或者视频参照,作为合规的证据代码用户的操作轨迹,也避免一些纠纷。

对于web 页面的轨迹追踪,还需要知道操作行为。一想到这个第一想到的就是在线直播学习课程中,找个屏幕录播软件,讲解员打开该软件,开始在电脑上的一切行为都被录制成视频。然后就可以直接把视频分享给所有的人。而你再讲解过程中的一些列细节都能够播放出来。所以当初就想让原生APP开发一个这样类似的东西。再webview中进行播放。这样功能也就ok了。但是还是需要支持移动端web 网页的轨迹追踪。

解决问题方式

思路一:Canvas
利用Canvas截图,使用 html2canvas 库,不停的画页面然后不停的截图,再讲图片播放出来。后来调研了一下~ 发现这个可操作性几乎未零。太复杂了,数据量右大,而我们的页面又复杂,canvas 画半天耗内存,有时候会卡主。直接pass了

思路二:记录页面DOM变化
主要原理是:MutationObserver接口提供了监视对DOM树所做更改的能力。我们可以利用这个接口,保存每次变化的DOM数据,然后把这些数据转换成可视化的数据结构,然后给一个个保存起来。然后我们使用特定的方式对一个个保存起来的DOM数据进行还原并重新渲染出来。DOM节点的变化也就意味了页面轨迹发送了变化。这样就可以把这些轨迹记录下来。我们只需要再把这个还原出来的DOM播放的过程中进行录制,这样就可以保持下来这些轨迹视频。

进行实践
后续调研,以及其他同事的帮助,我们找到了LogRocket ,专业的web app用户行为记录工具,官网描述:

LogRocket记录用户在你的web上做的一切事情,以帮助你重现bug并更快的解决他们
在你的web app中发现问题不应该如此艰难
用LogRocket,重现问题就像他们发生在你自己的浏览器中一样
按照其描述,以及我们的进一步了解,发现LogRocket 里面的一些东西确实可以帮助我们解决问题。但是主要问题是还是因为LogRocket 这些数据,我们无法获取到自己的库里面。只能是第三方的库。并不满足我们的需求,还是就是要用它,需要花钱。
大家想深入的了解,可以查看官网 LogRocket 官网

后来我们又了解到一个开源的 rrweb 框架。rrweb主要由3部分组成:

rrweb-snapshot,包括快照和重建功能。快照用于将DOM及其状态转换为具有唯一标识符的可序列化数据结构; 重建功能是将快照重建为相应的DOM。
rrweb,包括两个功能:记录和重播。记录功能用于记录DOM中的所有动作变化; 重放是根据相应的时间戳逐个重放记录下的动作变化。
rweb-player是 rrweb的一个玩家用户界面,可以随时提供基于GUI的功能,如暂停,快进,拖放等功能。
使用这些我们可用于web界面录制以及web界面重放这两个主要功能,rrweb-snapshot 返回的数据结构是json格式的,方便我们前后台数据对接,也规范了这款业务逻辑操作。

rrweb 实践
引入库
(1) 直接通过script引入,推荐通过 jsdelivr 的 CDN 安装:

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.css"
/>
<script src="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js"></script>

我们在被录制的应用中只需要引入录制部分代码即可,无需全量引入。

<script src="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/record/rrweb-record.min.js"></script>

(2) 通过 npm 引入rrweb 同时提供 commonJS 和 ES modules 两种格式的打包文件,易于和常见的打包工具配合使用。

npm install --save rrweb

兼容性
由于使用 MutationObserver API,rrweb 不支持 IE11 以下的浏览器。可以从mutationobserver兼容性列表中找到兼容的浏览器列表。

录制开始
如果通过 script 的方式仅引入录制部分,那么可以访问到全局变量 rrwebRecord,它和全量引入时的 rrweb.record 使用方式完全一致,具体看以下示例代码。

// 通过 <script> 的方式,全量引入、npm 引入的
start() {
	let self = this
	this.operation = rrweb.record({
	   emit(event) {
	     	self.events.push(event);
	   },
	});
}
// 通过 <script> 的方式,仅仅引入录制部分代码
start() {
	let self = this
	this.operation = rrwebRecord({
	   emit(event) {
	     	self.events.push(event);
	   },
	   recordCanvas: true //支持录制canvas
	   //其他参数
	});
}

rrweb 在录制时会不断将各类 event 传递给配置的 emit 方法,你可以使用任何方式存储这些 event 以便之后回放。
当前rrweb 只支持录制静态的canvas,无法录制动态js生成的canvas.

录制结束
录制结束,rrwebRecord再次执行一遍,即可停止录制。

stop() { 
    this.operation && this.operation()
}

录制数据压缩
引入pako 来对录制的数据进行 gzip压缩,

npm install pako --save
zip(str) {
    let binaryString = pako.gzip(encodeURIComponent(str), { to: 'string' })
    return btoa(binaryString);
}

因为录制的是web页面,需要对数据进行encodeURIComponent 一遍,不然解压之后的数据会变成乱码

录制数据解压

unzip(b64Data) { 
    var strData   = atob(b64Data);
    var charData  = strData.split('').map(function(x){return x.charCodeAt(0);});
    var binData   = new Uint8Array(charData);
    var data    = pako.inflate(binData);
    // strData   = String.fromCharCode.apply(null, new Uint16Array(data));
    let array = new Uint16Array(data)
    var res = '';
    var chunk = 8 * 1024;
    var i;
    for (i = 0; i < array.length / chunk; i++) {
    res += String.fromCharCode.apply(null, array.slice(i * chunk, (i + 1) * chunk)); 
    }
    res += String.fromCharCode.apply(null, array.slice(i * chunk));

    strData = res   
    return decodeURIComponent(strData);
}

Uint16Array 对字符的长度有限制,所以采用分段解压的方式,这样就不会出现内存溢出的问题。

录制数据保存

save(events) {
    let self = this
    if (self.events.length === 0 || events&&events.length === 0) {
        return Promise.resolve();
    }
    const data = JSON.stringify(self.events)
    let logsParams = { "message": data, "setMessageTime": (new Date().getTime() + '')}
    
    const body = JSON.stringify({
        "__topic__": "topic",
        "__source__": "source",
        "__logs__": [
            logsParams
        ]
    });
    let url = 'xxx保存地址urlxxxxx'
    return new Promise((resolve, reject) => {
        fetch(url, {
            method: 'POST',
            headers: {
                'x-log-apiversion': '0.6.0',
                'x-log-bodyrawsize': sizeof(body),
                'Content-Type': 'application/json',
            },
            body,
        }).catch(error => {
            reject(error)
        }).then(response => {
            resolve(response)
        })
    })
}

一个更接近实际真实使用场景的示例如下:

// 每 10 秒调用一次 save 方法,避免请求过多

setInterval(save, 10 * 1000);

// 每调用一次,清空数据

this.events = []

当前我们使用的阿里云的日志接口,也可以改成其他的类似的请求接口。类似官网差不多的。当然我们也可以根据各自的需求,改善自己提交数据的策略。

录制数据播放
(1) 如果 rrweb 是全局引入的情况下,回放时需要引入对应的 CSS 文件:

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.css"
/>

再通过以下 JS 代码初始化 replayer:

const events = YOUR_EVENTS;
const replayer = new rrweb.Replayer(events);
replayer.play();

在实际的应用场景,我们需要异步请求数据,在对数据进行解压,然后再调用JS 。

fetch(url).catch(error => {
    reject(error)
}).then(res => {
    //
    const eventsDate = JSON.stringify(res.data)
    const events = unzip(eventsDate)
    const replayer = new rrweb.Replayer(events);
	replayer.play();
})

(2) 使用 rrweb-player
rrweb-player 同样可以使用 CDN 方式安装:

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/style.css"
/>
<script src="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/index.js"></script>

或者通过 npm 安装:

npm install --save rrweb-player

使用方式:

new rrwebPlayer({
  target: document.body, // 可以自定义 DOM 元素
  data: {
    events,
  },
  UNSAFE_replayCanvas: true //支持回放 canvas 
});

具体其他的api,或者其他的使用方式,大家可以查看官网

参考网址:

rrweb:打开 web 页面录制与回放的黑盒子
rrweb 官网
rrweb-snapshot 快照
rrweb-player 播放
转载:https://blog.csdn.net/yingyangxing/article/details/108267549
参考:https://blog.csdn.net/blackcat88/article/details/88972515

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" />
    <button onclick="start()">start</button>
    <button onclick="stop()">stop</button>
    <button onclick="Player()">Player</button>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.css"
    />
    <script src="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js"></script>

    <script src="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/record/rrweb-record.min.js"></script>
    
    <!-- 录制视频 播放 -->
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/style.css"
    />
    <script src="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/index.js"></script>

    <script>
      let events = [],
        operation;
      function start() {
        // rrweb行为录制
        operation = rrweb.record({
          emit(event) {
            // 用任意方式存储 event
            console.log(event);
            events.push(event);
          },
        });
      }

      function stop() {
        operation && operation();
      }

      function Player() {
        new rrwebPlayer({
          target: document.body, // 可以自定义 DOM 元素
          data: {
            events,
          },
        });
      }

      
    </script>
  </body>
</html>

上一篇:随手记


下一篇:给文本注册单击事件不起作用问题记录