赞
踩
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 部分组成:
<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 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>
如果通过 <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/, '')
},
},
}
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!'))
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。