使用rrweb进行前端页面可回溯录制

背景

公司需要对用户的页面交互进行可回溯记录,固使用rrweb进行需求实现;

介绍

rrweb 全称 'record and replay the web',是当下很流行的一个录制屏幕的开源库。与我们传统认知的录屏方式(如 WebRTC)不同的是,rrweb 录制的不是真正的视频流,而是一个记录页面 DOM 变化的 JSON 数组,因此不能录制整个显示器的屏幕,只能录制浏览器的一个页签。

rrweb的github地址:rrweb/guide.zh_CN.md at master · rrweb-io/rrweb · GitHub

功能实现

  • 首先安装rrweb和rrweb-player

npm install --save rrweb
npm install --save rrweb-player
  • 创建screen-record.js文件对上报方法进行封装

rrweb 提供了一个基于 fflate 的简单压缩函数,在提交录制中可以作为 packFn 传入使用。

可以将录制流程分为不同的节点(stepIndex),与服务端商议好进行时间轴排序存储

import { record, pack, unpack } from 'rrweb'
import { saveTCDataByXhr, saveTCDataByBeacon, getTcKey, saveTCDataByFetch } from '@/api/record'

/**
 * 开始屏幕录制
 */
export function startRecord() {
  if (typeof window.stopRecordFn === 'function') {
    window.stopRecordFn()
  }
  window.screenRecords = []
  console.log('=========== Record Start ==========')
  const stopFn = record({
    emit(event) {
      window.screenRecords.push(event)
    },
    packFn: pack, // rrweb 内包含了基于 fflate 的简单压缩 rrweb.pack,在录制时可以作为 packFn 传入。
    inlineStylesheet: false,
    sampling: {
      scroll: 150, // 每 150ms 最多触发一次
      // set the interval of media interaction event
      media: 800,
      // input: 'last' // 连续输入时,只录制最终值
    },
  })
  window.stopRecordFn = stopFn
}
  •  提交录制数据

因为一般提交页面录制数据的时机为页面离开的时机,当浏览器中的某个页面发生终止时,不能保证进程中的HTTP请求会成功(请参阅有关“终止”和页面生命周期的其他状态的更多信息)。所以我们有如下几个解决方法:

  1. 使用async/await阻塞异步请求,直至请求发送完毕再进行路由跳转,但似乎这种方法牺牲了用户体验;
  2. 使用fetch请求的keepalive标识进行数据提交,但是会有浏览器兼容性问题,其兼容性如下图:
  3. 使用Navigator.sendBeacon(),该函数专门用于发送单向请求(信标)。但是此 API 不允许您发送自定义标头。因此,为了让我们以“application/json”的形式发送数据,我们需要做一些小调整并使用Blob:
    /**
     * beacon上传可回溯录制数据
     */
    export function saveTCDataByBeacon(data) {
      const url = `${RECORD_BASE_API}/tc/groOrder/saveTcData`
      // let data = new FormData();
      // for (let key in params) {
      //     data.append(key, params[key]);
      // }
      const headers = {
        type: 'application/json'
      }
      const blob = new Blob([JSON.stringify(data)], headers)
      return navigator.sendBeacon(url, blob)
    }
    

    但是注意:beacon只能提交少量数据,chrome限制最高64KB

基于上诉思路我们封装提交录制方法:

/**
 *
 * @param stepIndex 录制节点 立即投保: 100, 暂存: 200, 提交审核: 300, 支付: 400, 支付成功: 500
 * @param goodsCode
 * @param submitType 提交数据的方式 默认xhr,页面返回beacon
 */
export async function submitRecord(stepIndex, linkNo, submitType = 'xhr') {
  if (!window.screenRecords || !window.screenRecords.length) return
  const startEvent = unpack(window.screenRecords[0])
  const endEvent = unpack(window.screenRecords[window.screenRecords.length - 1])
  const { data: tcKey } = await getTcKey({ linkNo })
  const dataString = JSON.stringify(window.screenRecords)
  window.screenRecords.length = 0

  const params = {
    linkNo,
    tcKey,
    stepIndex,
    startTime: startEvent.timestamp,
    endTime: endEvent.timestamp,
    data: dataString,
  }
  // if (stepIndex >= 60) {
  //   params.linkNo = linkNo
  //   delete params.tcKey
  // }

  return new Promise((reslove, reject) => {
    // @ts-ignore
    if (navigator && navigator.sendBeacon && submitType === 'beacon') {
      // beacon只能提交少量数据,chrome限制最高64KB
      const result = saveTCDataByBeacon(params)
      if (result) {
        console.log('回溯数据请求成功排队 等待执行')
        reslove()
      } else {
        console.log('回溯数据提交失败')
        reject('回溯数据提交失败')
      }
    } else if (submitType === 'fetch') {
      saveTCDataByFetch(params).then(() => {
        reslove()
      }).catch(e => reject(e))
    } else {
      saveTCDataByXhr(params).then(() => {
        reslove()
      }).catch(e => reject(e))
    }
  })
}
/**
 * xhr上传可回溯录制数据
 */
export function saveTCDataByXhr(data) {
  return request({
    url: `${RECORD_BASE_API}/tc/groOrder/saveTcData`,
    method: 'post',
    data,
  })
}

/**
 * Fetch上传可回溯录制数据 将 keepalive 设置为 true 就可确保浏览器关闭或回退,调用接口的链接不会被关闭,调用成功
 */
export function saveTCDataByFetch(data) {
  const url = `${RECORD_BASE_API}/tc/groOrder/saveTcData`
  const headers = {
    'Content-Type': 'application/json;charset=UTF-8',
    [REQUEST_TOKEN_KEY]: getToken()
  }
  return new Promise((resolve, reject) => {
    fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
      // keepalive: true,
    })
      .then(res => {
        resolve(res.json()) 
      })
      .catch(error => {
        reject(error) 
      })
  })
}

页面使用

开始录制:

startRecord()

提交录制数据:

submitRecord(300, this.baseInfo.grpLinkNo)

页面回放:

</template>    
  <!-- 回放容器 -->
  <div ref="replayContainer" class="replay-container flex justify-center" />
</template>

<script setup>
  import { ref, onMounted} from 'vue';
  import RrwebPlayer from 'rrweb-player';
  import { unpack } from 'rrweb';

  const replayContainer = ref();
  const events = ref([]) // 回放数据通过服务端获取
  
  onMounted(() => {
     new RrwebPlayer({
         target: replayContainer.value,
         unpackFn: unpack,
         props: {
            events: events, // 包含回放所需的数据
            skipInactive: true, // 是否快速跳过无用户操作的阶段
            autoPlay: false, // 是否自动播放
            UNSAFE_replayCanvas: true, // 回放时是否回放 canvas 内容,开启后将会关闭沙盒策略,导致一定风险
            mouseTail: false, // 是否在回放时增加鼠标轨迹
          },
        })
     })
  })
</script>

  • 23
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值