基于vue3+vite+阿里云sls開(kāi)發(fā)的一個(gè)簡(jiǎn)單的性能監(jiān)控平臺(tái),可以進(jìn)行用戶行為分析,如:用戶數(shù)量,熱點(diǎn)頁(yè)面訪問(wèn)量,訪問(wèn)時(shí)段,用戶瀏覽器使用,分辨率等等數(shù)據(jù),以及前端性能相關(guān)數(shù)據(jù),如:頁(yè)面加載時(shí)間,js報(bào)錯(cuò)相關(guān)信息,接口加載時(shí)間,接口調(diào)用次數(shù)等等。下面廢話不多說(shuō),直接開(kāi)始項(xiàng)目。
1.vue3+vite初始化項(xiàng)目
1. npm create vite 項(xiàng)目名稱
2. cd項(xiàng)目名稱
3. npm install 《安裝項(xiàng)目依賴包》
4. npm run dev 《啟動(dòng)項(xiàng)目》
選擇框架的時(shí)候選擇vue+ts,完成之后就獲得了一個(gè)基礎(chǔ)版本的項(xiàng)目。大致目錄如下:

你會(huì)發(fā)現(xiàn)script用到了setup語(yǔ)法糖,

第一次見(jiàn)到的小伙伴可能會(huì)一臉懵,這里先附上官網(wǎng)文檔以及一份寫的不錯(cuò)的setup語(yǔ)法糖,總結(jié)就是變量不用return了,組件不用注冊(cè)了,props和emits換方法了,父組件要調(diào)用子組件內(nèi)容,需要把子組件變量或者方法defineExpose({})出去,等等。
https://v3.cn.vuejs.org/api/sfc-script-setup.html#%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95
https://mp.weixin.qq.com/s/-1VRzekOKvK4ymEN8mHAFg
vue3不再使用elementui,而是改成了element-plus,這點(diǎn)大家也要注意,附上官網(wǎng):
https://element-plus.gitee.io/zh-CN/
element-plus自身支持國(guó)際化,有點(diǎn)比較麻煩的是里面的icon需要用到哪個(gè),就要在文件里import哪個(gè),沒(méi)有原來(lái)方便了,可以使用注冊(cè)全局組件的方法,把所有ICON注入:
import * as Icons from '@element-plus/icons'
Object.keys(Icons).forEach(key => {
app.component(key, Icons[key as keyof typeof Icons])
})
以上這些只是隨意提一嘴,屬于基礎(chǔ)內(nèi)容,打造前端性能監(jiān)控平臺(tái),最主要的在于阿里云sls日志服務(wù)的使用。

也就是這個(gè)東西,開(kāi)通服務(wù),然后新建Project:

然后再新建一個(gè)Logstore

此時(shí)切記一定要打開(kāi)WebTracking,這是專門為前端來(lái)采集信息的。最好打開(kāi)永久保存。
創(chuàng)建完畢之后會(huì)問(wèn)你是否立即接入數(shù)據(jù),選擇是,然后選擇webTracking接入數(shù)據(jù):


接入之后,阿里云的基本配置也就準(zhǔn)備好了,下一步就是去我們的項(xiàng)目里進(jìn)行埋點(diǎn),并且使用阿里云的WebTracking SDK將埋點(diǎn)數(shù)據(jù)傳到阿里云sls上。
這里附上官方文檔地址:
https://help.aliyun.com/document_detail/31752.html?spm=5176.2020520112.0.0.377f34c0q0DTnF#h4--http-get-

這里我們嘗試上傳一個(gè)js埋點(diǎn)報(bào)錯(cuò)數(shù)據(jù):
//監(jiān)聽(tīng)頁(yè)面Js報(bào)錯(cuò)
function injectJsError(){
window.addEventListener('error',function(event){
console.log('------addEventListener-error-------')
console.log(event)
let log = {
kind:'stability',//監(jiān)控指標(biāo)大類 穩(wěn)定性
type:'error',//小類
errorType:'jsError',
message:event.message,
filename:event.filename,
position:`${event.lineno}:${event.colno}`,
stack:event.error&&event.error.stack?getLines(event.error.stack):'-'
}
SentTracker.sendMsg(log);
// console.log(log)
})
}
class SentTracker{
url;
xhr;
constructor(){
this.url = `https://${ProjectName}.${Endpoint}/logstores/${logstoreName}/track`;
this.xhr = new XMLHttpRequest;
}
//上報(bào)單條埋點(diǎn)信息
sendMsg(data = {}){
let extraData = getExtraData();
let logInfo = {...extraData,...data}
for(let key in logInfo){
if(typeof logInfo[key] === 'number'){ //如果是數(shù)字 需要轉(zhuǎn)為字符串,阿里要求的傳參格式
logInfo[key] = `${logInfo[key]}`
}
}
logger.send(logInfo)
}
//上報(bào)多條埋點(diǎn)信息
sendMsgs(data = []){
let extraData = getExtraData();
let logArr= [logInfo]
data.forEach((item)=>{
let logInfo = {...extraData,...item}
for(let key in item){
if(typeof item[key] === 'number'){ //如果是數(shù)字 需要轉(zhuǎn)為字符串,阿里要求的傳參格式
item[key] = `${item[key]}`
}
}
logArr.push(logInfo)
})
let body = JSON.stringify(logInfo)
this.xhr.open('POST',this.url,true);
this.xhr.setRequestHeader('Content-Type','application/json');
this.xhr.setRequestHeader('x-log-apiversion','0.6.0');
this.xhr.setRequestHeader('x-log-bodyrawsize',body.length);
this.xhr.setRequestHeader('x-log-compresstyp','lz4');
this.xhr.onload = ()=>{
console.log(this.xhr.response)
}
this.xhr.onerror = (error)=>{
console.log(error)
}
let newBody = {
"__topic__": topic,
"__source__": source,
"__logs__":logArr,
"__tags__": {
"tag": "日志標(biāo)簽"
}
}
this.xhr.send(JSON.stringify(newBody));
}
}
當(dāng)監(jiān)聽(tīng)到j(luò)s報(bào)錯(cuò)的時(shí)候,項(xiàng)目就會(huì)自動(dòng)把錯(cuò)誤信息上傳到sls里

當(dāng)時(shí)我在想,如果我要統(tǒng)計(jì)后端接口的調(diào)用次數(shù),那是不是每次調(diào)用接口都要往sls里發(fā)起一次請(qǐng)求,那對(duì)于我項(xiàng)目的性能不是會(huì)有很大影響么,但是sls其實(shí)也早就想到了這點(diǎn),所以它有兩個(gè)配置項(xiàng):
opts = {
host: '',
project: '',
logstore: '',
time: 10, //10s內(nèi)自動(dòng)收集日志,10s發(fā)一次
count: 30, //最多收集30條
}
可以看到,他是可以自動(dòng)收集埋點(diǎn)數(shù)據(jù),然后每隔一段時(shí)間發(fā)起一次請(qǐng)求的。
我們?cè)偃タ纯窗⒗镌迫罩編?kù):

所有上傳的埋點(diǎn)信息,都會(huì)存進(jìn)去,可以理解為一張數(shù)據(jù)庫(kù)表,下一步,就是把這些表數(shù)據(jù)收集整理,然后做成可視化圖表就行了,只要會(huì)簡(jiǎn)單的sql語(yǔ)句就行。
這里有個(gè)坑,我當(dāng)時(shí)按照文檔用Js去查日志庫(kù)里的數(shù)據(jù),死活查不出來(lái),后來(lái)才發(fā)現(xiàn),必須用node.js去查才可以,這里附上node關(guān)鍵代碼:
const ALY = require('aliyun-sdk')
var sls = new ALY.SLS({
accessKeyId: "", //阿里云訪問(wèn)密鑰AccessKey ID。更多信息,請(qǐng)參見(jiàn)訪問(wèn)密鑰。阿里云賬號(hào)AccessKey擁有所有API的訪問(wèn)權(quán)限,風(fēng)險(xiǎn)很高。強(qiáng)烈建議您創(chuàng)建并使用RAM用戶進(jìn)行API訪問(wèn)或日常運(yùn)維。
secretAccessKey: "", //阿里云訪問(wèn)密鑰AccessKey Secret。
endpoint: '', //日志服務(wù)的域名。更多信息,請(qǐng)參見(jiàn)服務(wù)入口。此處以杭州為例,其它地域請(qǐng)根據(jù)實(shí)際情況填寫。
apiVersion: '2015-06-01' //SDK版本號(hào),固定值。
})
const projectName = "" // 必選,Project名稱。
const logstoreName = "" // 必選,Project描述。
// 查詢?nèi)罩尽?function queryLog(query, callback) {
const param = {
projectName, // 必選,Project名稱。
logStoreName: logstoreName, // 必選,Logstore名稱。
from: query.from, // 必選,開(kāi)始時(shí)間,精度為秒。
to: query.to, // 必選,結(jié)束時(shí)間,精度為秒
topic: query.topic, // 可選,指定日志主題。
query: query.query // 可選,查詢的關(guān)鍵詞,不輸入則查詢?nèi)咳罩緮?shù)據(jù)。
}
sls.getLogs(param, function (err, data) {
if (err) {
let obj = {
code: 500,
data: '',
msg: err
}
callback(obj)
} else {
let obj = {
code: 0,
data: data,
msg: 'success'
}
callback(obj)
}
})
}
然后通過(guò)前端調(diào)用后端Node的接口,前端就可以獲取到日志庫(kù)里面的數(shù)據(jù)了,node只是為了前端獲取數(shù)據(jù)做一道中轉(zhuǎn)而已。附上前端sql語(yǔ)句的示例
//獲取js執(zhí)行報(bào)錯(cuò)統(tǒng)計(jì)
let jsError = await getLogs(
proxy.$urlData.getLogs,
"* and kind : stability and type : error and errorType : jsError | SELECT message,url, count(url) AS PV where message!='null' GROUP BY message,url ORDER BY PV DESC"
);
此時(shí)一個(gè)性能監(jiān)控平臺(tái)就搭建好了:
