背景
公司需要对用户的页面交互进行可回溯记录,固使用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 rrwebnpm 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
请求会成功(请参阅有关“终止”和页面生命周期的其他状态的更多信息)。所以我们有如下几个解决方法:
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>