rrweb 是 ‘record and replay the web’ 的简写,旨在利用现代浏览器所提供的强大 API 录制并回放任意 web 界面中的用户操作。
rrweb中文文档 https://github.com/rrweb-io/rrweb/blob/master/guide.zh_CN.md
本文项目地址 https://github.com/qdfudimo/vue-rrweb 大家点个星
demo介绍
该项目分为客户端和服务端
- 客户端 vue3
cd ./rrweb-client
执行pnpm dev
- 服务端使用 node 通过上传的json数据保存在本地文件中,在调用接口的时候再读取文件内容返回给后台
cd ./rrweb-serve
执行pnpm dev
rrweb 介绍
rrweb 主要由 3 部分组成:
- rrweb-snapshot,包含 snapshot 和 rebuild 两个功能。snapshot 用于将 DOM 及其状态转化为可序列化的数据结构并添加唯一标识;rebuild 则是将 snapshot 记录的数据结构重建为对应的 DOM。
- rrweb,包含 record 和 replay 两个功能。record 用于记录 DOM 中的所有变更(mutation);replay 则是将记录的变更按照对应的时间一一重放。
- rrweb-player,为 rrweb 提供一套 UI 控件,提供基于 GUI 的暂停、快进、拖拽至任意时间点播放等功能。
使用指南
直接通过 <script>
引入
<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>
通过 npm 引入
npm install --save rrweb rrweb-player
rrweb 同时提供 commonJS 和 ES modules 两种格式的打包文件,易于和常见的打包工具配合使用。
兼容性
由于使用 MutationObserver
API,rrweb 不支持 IE11 以下的浏览器。可以从这里找到兼容的浏览器列表。
隐私
页面中可能存在一些隐私相关的内容不希望被录制,rrweb 为此做了以下支持:
- 在 HTML 元素中添加类名
.rr-block
将会避免该元素及其子元素被录制,回放时取而代之的是一个同等宽高的占位元素。 - 在 HTML 元素中添加类名
.rr-ignore
将会避免录制该元素的输入事件。 - 所有带有
.rr-mask
类名的元素及其子元素的 text 内容将会被屏蔽。 input[type="password"]
类型的密码输入框默认不会录制输入事件。- 配置中还有更为丰富的隐私保护选项。
快速开始
<template>
<div id="replay" ref="replay" v-if="isPlaying" />
<textarea placeholder-class="textarea-placeholder" />
<button type="button" @click="handelStart">开始录制</button>
<button type="button" @click="handelPasue">暂停</button>
<button type="button" @click="handelRecord">回放</button>
<button type="button" @click="handelReRecord">网络回放</button>
<button type="button" @click="handelRequest">请求</button>
<button type="button" @click="handelPayStart">支付开始</button>
<button type="button" @click="handelPayEnd">支付结束</button>
<div class="modal rr-mask" v-if="isRecording">正在录制</div>
</template>
录制
如果通过 <script>
的方式仅引入录制部分,那么可以访问到全局变量 rrwebRecord
,它和全量引入时的 rrweb.record
使用方式完全一致,以下示例代码将使用后者。
import * as rrweb from 'rrweb';
import rrwebPlayer from 'rrweb-player';
import 'rrweb-player/dist/style.css';
import { ref, onUnmounted } from 'vue';
import axios from "axios";
const events = ref([])
const replay = ref()
/**是否正在回放 */
const isPlaying = ref(false)
/**是否正在录制 */
const isRecording = ref(false)
let stopFn = null
let replayInstance = null;
const handelStart = () => {
isPlaying.value = false;
isRecording.value = true;
events.value = []
stopFn = rrweb.record({
emit(event) {
// 用任意方式存储 event
events.value.push(event)
// 以 rrwebEvents 的长度作为分片持续上传 防止数据过大
if (events.value.length >= 100) {
//超过100 上传给后台 同时重置为空
uploadFile()
events.value = []
}
},
});
}
rrweb 在录制时会不断将各类 event 传递给配置的 emit 方法,你可以使用任何方式存储这些 event 以便之后回放。
调用 record
方法将返回一个函数,调用该函数可以终止录制:
停止录制
/**let stopFn = rrweb.record({
emit(event) {
if (events.length > 100) {
// 当事件数量大于 100 时停止录制
stopFn();
}
},
}); */
/**暂停录屏且上传 */
const handelPasue = () => {
isRecording.value = false
stopFn()
if (events.value.length === 0) return
uploadFile();
events.value = []
}
上传数据
/**
* 压缩 events 数据,并上传至后端
*用于将 events 发送至后端存入,并重置 events 数组
*/
const uploadFile = () => {
console.log("上传快照了");
axios('/apis/uploadFile', {
method: 'post',
headers: {
'Content-type': 'application/json'
},
data: JSON.stringify({
events: events.value
})
})
.then(response => {
console.log('response', response)
})
.catch(error => {
console.log('error', error)
})
}
回放
npm install --save rrweb-player
import rrwebPlayer from 'rrweb-player';
import 'rrweb-player/dist/style.css';
使用
通过 props 传入 events 数据及配置项
/**new rrwebPlayer({
target: document.body, // 可以自定义 DOM 元素
// 配置项
props: {
events,
},
}); */
const handelRecord = () => {
if (isRecording.value) {
console.log("请先暂停录制");
return
}
isPlaying.value = true
//vue异步更新 为了获取到replay dom
setTimeout(() => {
replayInstance = new rrwebPlayer({
target: replay.value, // 可以自定义 DOM 元素
// 配置项
props: {
events: events.value,
skipInactive:false, //是否快速跳过无用户操作的阶段
showDebug: false, //是否在回放过程中打印 debug 信息
showWarning: false, //是否在回放过程中打印警告信息
autoPlay: true, //是否自动播放
showControlle :true,//是否显示播放器控制 UI
speedOption:[1, 2, 4, 8] //倍速播放可选值
},
});
replayInstance.addEventListener("finish", (payload) => {
console.log(payload,2222);
})
}, 100);
}
请求后台数据
/**网络请求回放 */
const handelReRecord = () => {
axios('/apis/getFile', {
method: 'post'
})
.then(res => {
if (res.data.code == 200) {
let { data = [] } = res.data
if (data.length) {
events.value = data;
// replayInstance.destroy()
// replayInstance = null
handelRecord()
}
}
})
.catch(error => {
console.log('error', error)
})
}
vite配置跨域
server: {
proxy: { // 跨域代理
'/apis': {
// target: 'http://' + env.VUE_APP_BASE_API,
target: 'http://localhost:3000/', //
changeOrigin: true,
logLevel: 'debug',
rewrite: (path) => path.replace(/^\/apis/, '')
},
},
}
node服务端
pnpm i express body-parser
const express = require("express")
const bodyParser = require('body-parser')
const fs = require('fs')
const path = require('path')
const app = express()
app.use(bodyParser.urlencoded({
extended: false
}))
app.use(bodyParser.json())
app.post("/uploadFile",(req,res)=>{
console.log(req.body,11);
const jsonFile = path.join(process.cwd(), `./file/jsonFile${Date.now()}.json`)
fs.writeFileSync(jsonFile, JSON.stringify(req.body.events))
res.send({
data:"",
msg:"上传成功",
code:200
})
})
app.post("/getFile",(req,res)=>{
const fileDirPath = path.join(process.cwd(), `./file`);
const files = fs.readdirSync(fileDirPath);
console.log(files);
let file;
if(files && files.length) {
file = fs.readFileSync(`${fileDirPath}/${files[files.length-1]}`); // 此处只取第一个文件片段验证
}
res.send({
data:JSON.parse(file),
msg:"上传成功",
code:200
})
})
// 清理文件内容
app.post('/clearFile', ctx => {
const fileDirPath = path.join(process.cwd(), `./file`);
const files = fs.readdirSync(fileDirPath);
if(files && files.length) {
files.forEach(item => {
const filePath = `${fileDirPath}/${item}`;
fs.unlinkSync(filePath);
})
}
ctx.response.body = {
status: '00'
}
})
app.get("/count",(req,res)=>{
res.send("1111")
})
// 2. 设置请求对应的处理函数
// 当客户端以 GET 方法请求 / 的时候就会调用第二个参数:请求处理函数
app.get('/', (req, res) => {
res.send('hello world')
})
// 3. 监听端口号,启动 Web 服务
app.listen(3000, () => console.log('app listening on port 3000!'))