前端如何靜悄悄錄制用戶的操作過(guò)程,靜悄悄上傳到服務(wù)器
背景
公司有很多的項(xiàng)目,但是并不是每一個(gè)項(xiàng)目都很重要,其實(shí)重要的項(xiàng)目就那么幾個(gè),上面也是很重視這幾個(gè)項(xiàng)目,尤其是對(duì)一些生產(chǎn)問(wèn)題的關(guān)注度很高。
這幾天上面交代下來(lái)了,需要對(duì)這些項(xiàng)目做一些用戶行為的記錄,主要是為了更好地還原用戶在某一個(gè)時(shí)間點(diǎn)的操作過(guò)程
注意點(diǎn)
想要完成這個(gè)需求,仔細(xì)想了一下,需要注意幾個(gè)點(diǎn):
- 跨框架使用:這些項(xiàng)目有vue、angular、react,需要都能適用
- 能錄制用戶行為:能把用戶在頁(yè)面上的操作錄制下來(lái)
- 能回放錄制:如果不能回放,那么這個(gè)錄制就無(wú)意義了
- 用戶無(wú)感知:必須做到用戶無(wú)感知才行
思考 & 技術(shù)方案
說(shuō)到前端視頻的錄制,我們會(huì)想到 webRTC
這個(gè)技術(shù),他能做到錄制屏幕的效果,但是通過(guò) webRTC
去完成這個(gè)方案的話,有幾個(gè)缺點(diǎn):
- 做不到用戶無(wú)感知,需要用戶同意才能錄制
- 錄制的視頻太大了,太占內(nèi)存了
- 學(xué)習(xí)成本比較高,這也是原因之一
那怎么才能做到:
- 用戶無(wú)感知
- 不錄制視頻
其實(shí)只要不錄制視頻了,那么用戶肯定就無(wú)感知,因?yàn)橐坏┮浺曨l,瀏覽器肯定要詢問(wèn)用戶同意不同意。
所以我們選擇了另一個(gè)方案 rrweb
,一個(gè)用來(lái)錄制用戶頁(yè)面行為的庫(kù)~
rrweb
rrweb
是 record and replay the web
的簡(jiǎn)寫,旨在利用現(xiàn)代瀏覽器所提供的強(qiáng)大 API 錄制并回放任意 web 界面中的用戶操作。
效果展示
基本使用
我們先定義好 html 結(jié)構(gòu),三個(gè)按鈕
- 錄制:點(diǎn)擊開(kāi)始錄制
- 回放:點(diǎn)擊開(kāi)始回放
- 返回:點(diǎn)擊重新再來(lái)
還有一個(gè) replayer ,用來(lái)當(dāng)做回放的容器
<template>
<div class="main">
<div >
<el-button @click="record">錄制</el-button>
<el-button @click="replay">回放</el-button>
<el-button @click="reset">返回</el-button>
</div>
<div v-if="!showReplay">
<div>
<el-input style="width: 300px" v-model="value" />
</div>
<div>
<el-button>按鈕1</el-button>
</div>
<div>
<el-button>按鈕2</el-button>
</div>
<div>
<el-button>按鈕3</el-button>
</div>
</div>
<div ref="replayer"></div>
</div>
</template>
我們需要先安裝兩個(gè)包:npm i rrweb rrwebPlayer
- rrweb:用來(lái)錄制網(wǎng)頁(yè)的
- rrwebPlayer:用來(lái)回放的
rrweb
擁有一個(gè) record
函數(shù)來(lái)進(jìn)行錄制操作,并可傳入配置,emit
屬性就是用戶操作的監(jiān)聽(tīng)函數(shù),接收一個(gè)參數(shù)event
,這個(gè)參數(shù)是什么,我們后面會(huì)說(shuō)~
然后我們定義三個(gè)函數(shù):
- record:錄制函數(shù)
- replay:回放函數(shù)
- reset:返回/重置函數(shù)
const rrweb = require("rrweb");
import rrwebPlayer from "rrweb-player";
const events = ref([]);
const stopFn = ref(null);
const showReplay = ref(false);
const replayer = ref(null)
const record = () => {
console.log("開(kāi)始錄制");
stopFn.value = rrweb.record({
emit: (event) => {
events.value.push(event);
},
// 支持錄制canvas
recordCanvas: true,
collectFonts: true,
});
};
const replay = () => {
stopFn.value();
showReplay.value = true;
new rrwebPlayer({
// 可以自定義 DOM 元素
target: replayer.value,
// 配置項(xiàng)
props: {
// 傳入events
events: events.value,
},
});
};
const reset = () => {
showReplay.value = false;
events.value = []
};
錄的是視頻嗎?
我們之前說(shuō)了:一旦要錄視頻,瀏覽器肯定要詢問(wèn)用戶同意不同意。但是我們發(fā)現(xiàn)我們使用 rrweb
去錄制,瀏覽器并沒(méi)有詢問(wèn),做到了無(wú)感知~所以我們可以推斷出,rrweb
錄制的并不是視頻,那錄制的是什么呢?
我們其實(shí)可以試著去輸出一下剛剛的參數(shù) event
看看是什么
rrweb.record({
emit: (event) => {
// 輸出
+ console.log(event)
events.value.push(event);
},
});
我們可以看到這個(gè)event
記錄的東西是當(dāng)前頁(yè)面的DOM
結(jié)構(gòu),當(dāng)用戶操作頁(yè)面時(shí),rrweb
會(huì)將每一次的DOM結(jié)構(gòu)轉(zhuǎn)換成對(duì)象形式,通過(guò) emit
函數(shù)的第一個(gè)參數(shù)輸出,我們使用一個(gè)數(shù)組去記錄這一次次的DOM結(jié)構(gòu),然后把它傳給rrweb-player
,它能將這些DOM結(jié)構(gòu)按照先后順序,一個(gè)一個(gè)展示出來(lái),自然就相當(dāng)于是視頻的展示效果了~
rrweb
能記錄這些頁(yè)面的 DOM 行為:
- 節(jié)點(diǎn)創(chuàng)建、銷毀
- 節(jié)點(diǎn)屬性變化
- 文本變化
- 鼠標(biāo)移動(dòng)
- 鼠標(biāo)交互
- 頁(yè)面或元素滾動(dòng)
- 視窗大小改變
- 輸入
上傳 & 優(yōu)化
我們記錄的這些數(shù)據(jù),需要上傳到后端那邊去,方便后續(xù)在后臺(tái)管理系統(tǒng)里管理這些回放~
很多人會(huì)說(shuō)這樣一直錄制,那豈不是數(shù)據(jù)量很大?所以我覺(jué)得只有在一些重要的頁(yè)面,才需要做錄制行為的操作,而不是每一個(gè)頁(yè)面都去做這樣的操作~
并且每次上傳需要一定的時(shí)間間隔,不能上傳太頻繁,不然瀏覽器壓力會(huì)增大~
const record = () => {
console.log("開(kāi)始錄制");
stopFn.value = rrweb.record({
emit: (event) => {
events.value.push(event);
},
recordCanvas: true,
collectFonts: true,
});
};
const report = async () => {
await reportRequest(events.value);
events.value = [];
}
// 20s 去上傳一次
setInterval(report, 10 * 2000);
同時(shí),雖然現(xiàn)在上傳的是DOM結(jié)構(gòu)的對(duì)象,大小遠(yuǎn)遠(yuǎn)比視頻小,但是其實(shí)還是不小的
所以我們需要采取措施,去壓縮一下數(shù)據(jù),壓縮后再進(jìn)行上傳,這樣能降低服務(wù)器的壓力~我們可以使用packFn
屬性來(lái)對(duì)錄制的數(shù)據(jù)進(jìn)行壓縮,同時(shí)回放時(shí)也要用unpackFn
去解碼
const record = () => {
console.log("開(kāi)始錄制");
stopFn.value = rrweb.record({
emit: (event) => {
events.value.push(event);
},
recordCanvas: true,
collectFonts: true,
+ packFn: rrweb.pack
});
};
const replay = () => {
stopFn.value();
showReplay.value = true;
new rrwebPlayer({
// 可以自定義 DOM 元素
target: replayer.value,
// 配置項(xiàng)
props: {
// 傳入events
events: events.value,
+ unpackFn: rrweb.unpack,
},
});
};