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 大家点个星
该项目分为客户端和服务端
cd ./rrweb-client
执行 pnpm dev
cd ./rrweb-serve
执行 pnpm dev
rrweb 主要由 3 部分组成:
引入<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 install --save rrweb rrweb-player
rrweb 同时提供 commonJS 和 ES modules 两种格式的打包文件,易于和常见的打包工具配合使用。
由于使用 MutationObserver
API,rrweb 不支持 IE11 以下的浏览器。可以从这里找到兼容的浏览器列表。
页面中可能存在一些隐私相关的内容不希望被录制,rrweb 为此做了以下支持:
.rr-block
将会避免该元素及其子元素被录制,回放时取而代之的是一个同等宽高的占位元素。.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>
如果通过 的方式仅引入录制部分,那么可以访问到全局变量
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/, '')
},
},
}
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!'))