当前位置:   article > 正文

前端录制回放rrweb_rrweb文档

rrweb文档

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介绍

该项目分为客户端服务端

  1. 客户端 vue3 cd ./rrweb-client 执行 pnpm dev
  2. 服务端使用 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>
  • 1
  • 2
  • 3
  • 4
  • 5

通过 npm 引入

npm install --save rrweb rrweb-player
  • 1

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

录制

如果通过 <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 = []
      }
    },
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

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 = []
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

上传数据

/**
 *  压缩 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)
    })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

回放

npm install --save rrweb-player
  • 1
import rrwebPlayer from 'rrweb-player';
import 'rrweb-player/dist/style.css';
  • 1
  • 2
使用

通过 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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

请求后台数据

/**网络请求回放 */
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)
    })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

vite配置跨域

 server: {
    proxy: { // 跨域代理
      '/apis': {
        // target: 'http://' + env.VUE_APP_BASE_API,
        target: 'http://localhost:3000/', // 
        changeOrigin: true,
        logLevel: 'debug',
        rewrite: (path) => path.replace(/^\/apis/, '')
      },
    },
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

node服务端

pnpm i express body-parser
  • 1
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!'))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/714600
推荐阅读
相关标签
  

闽ICP备14008679号