官方文档: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>