前言
在當(dāng)下前后端分離的主流環(huán)境下闸盔,前端部分的優(yōu)化變得越來越重要扳还。為了提升前端的性能和用戶體驗(yàn)哄陶,我覺得可能需要從三個(gè)維度采集數(shù)據(jù)進(jìn)行分析帆阳。
- 前端埋點(diǎn)。通過埋點(diǎn)收集和統(tǒng)計(jì)網(wǎng)頁的UV/PV屋吨、設(shè)備型號(hào)蜒谤、瀏覽器等數(shù)據(jù)進(jìn)行分析山宾,比如可以有針對(duì)性對(duì)使用比較靠前的設(shè)備、瀏覽器等做優(yōu)化和體驗(yàn)芭逝。
- 網(wǎng)頁性能收集和監(jiān)控塌碌。 采集一個(gè)頁面從請(qǐng)求開始到完成這個(gè)過程中的數(shù)據(jù)指標(biāo)。比如收集和監(jiān)控首屏加載時(shí)間旬盯、dom渲染時(shí)長(zhǎng)台妆、響應(yīng)比較慢的接口等。有了這些數(shù)據(jù)可以很直觀和針對(duì)性的對(duì)網(wǎng)頁的性能進(jìn)行優(yōu)化和升級(jí)胖翰。
- 錯(cuò)誤收集和監(jiān)控接剩。收集網(wǎng)頁中的js的報(bào)錯(cuò)、靜態(tài)資源加載報(bào)錯(cuò)萨咳,保證網(wǎng)頁正常訪問和降低bug率懊缺。
1.前端埋點(diǎn)
采集用戶的行為,監(jiān)控產(chǎn)品在用戶端的使用情況培他,根據(jù)數(shù)據(jù)可以明確鹃两,前端可以針對(duì)性的做優(yōu)化和體驗(yàn)。
可以簡(jiǎn)單的初步建立這樣的數(shù)據(jù)模型:
{
ip: ip地址舀凛,統(tǒng)計(jì)uv
ua: 瀏覽器的userAgent //方便區(qū)分瀏覽器的品牌
os: 系統(tǒng)名稱
current_page_all: 當(dāng)前頁面訪問總次數(shù)
width:瀏覽器寬度,
height:瀏覽器高度, //統(tǒng)計(jì)瀏覽器的尺寸
current_enter_time: 進(jìn)入當(dāng)前頁面的時(shí)間戳
current_leave_time: 離開當(dāng)前頁面的時(shí)間戳
project_id: 項(xiàng)目的id,
url: 當(dāng)前url,
user_uni_id: 臨時(shí)分配給用戶唯一的id
....
還有其他
}
2.在網(wǎng)頁中植入對(duì)應(yīng)的js代碼
//進(jìn)入
document.addEventListener('DOMContentLoaded',function(){
...
let args = {
ua: navigator.userAgent,
os: navigator.platform,
width: document.body.clientWidth || document.documentElement.clientWidth,
height: document.body.clientHeight || document.documentElement.clientHeight,
project_id: md5('abcd'),
user_uni_id: '臨時(shí)分配給用戶唯一的id',
url: window.location.href,
current_enter_time: new Date().getTime()
....
};
let img = new Image();
img.onload = function() {
img = null;
};
img.src= `https://localhost:9700/bury.gif?args=${qs(args)}`;
});
//離開
window.onbeforeunload = function() {
...
let args = {
...
leave_time: new Date().getTime()
....
};
let img = new Image();
img.onload = function() {
img = null;
};
img.src= `https://localhost:9700/bury.gif?args=${qs(args)}`;
}
- 收集數(shù)據(jù)并上報(bào)
const fs = require('fs');
route.get('/bury.gif',async (ctx)=>{
let queryStr = ctx.querystring;
let d = new Date();
let year = d.getFullYear();
let month = d.getMonth()+1;
let day = d.getDate()+1;
fs.writeFile(`../logs/${year}-${month}-${day}.log`,queryStr,{flag:'a',encoding:'utf-8',mode:'0666'},function(e){});
ctx.status = 200;
ctx.type = 'image/gif';
ctx.body = {};
});
簡(jiǎn)單的設(shè)想方案是先把數(shù)據(jù)收集到文本文件里俊扳,然后定時(shí)的分析這些文本文件,然后把篩選后的放到數(shù)據(jù)庫中猛遍。
2.網(wǎng)頁性能收集
網(wǎng)頁性能主要是收集是輸入url地址到網(wǎng)頁請(qǐng)求完成資源下載完成這段時(shí)間范圍內(nèi)一些請(qǐng)求馋记、加載等指標(biāo)。
比如舉幾個(gè)簡(jiǎn)單的指標(biāo):
首屏加載時(shí)長(zhǎng)
HTML 文檔被加載和解析完成
網(wǎng)頁加載完成時(shí)間
白屏?xí)r間
其他…
- 具體實(shí)現(xiàn)方案(一):
//index.html
<html>
<head>
<script>
window.startTime = Date.now();
</script>
</head>
<body>
<script>
let diff = Date.now()-window.startTime;
console.log('白屏?xí)r長(zhǎng)'+diff);
document.addEventListener('DOMContentLoaded',()=>{
console.log('HTML 文檔被加載和解析完成時(shí)間'+Date.now()-window.startTime);
});
window.onload = function() {
console.log('網(wǎng)頁加載完成時(shí)間'+Date.now()-window.startTime);
};
</script>
<script src="a.js"></script>
<script src="b.js"></script>
....
</body>
</html>
- 使用performance API
Performance是W3C性能小組引入進(jìn)來的一個(gè)新的API懊烤,他可以很好的獲取到首屏加載時(shí)間梯醒、白屏?xí)r間、dns查詢時(shí)間等腌紧,是一個(gè)很方便的獲取網(wǎng)頁性能指標(biāo)的API,而且目前大部分主流瀏覽器是支持的茸习。
https://www.caniuse.com/
1.Performance一些常用用法的總結(jié)
let timing = window.performance.timing
//白屏?xí)r間
timing.responseStart - timing.navigationStart
//DNS 查詢時(shí)長(zhǎng)
timing.domainLookupEnd - timing.domainLookupStart
//request請(qǐng)求耗時(shí)
timing.responseEnd - timing.responseStart
//HTML 文檔被加載和解析完成耗時(shí)
timing.domComplete - timing.domInteractive
//網(wǎng)頁加載完成耗時(shí)
timing.loadEventEnd - timing.navigationStart
//重定向耗時(shí)
timing.redirectEnd - timing.redirectStart;
//占用的內(nèi)存
window.performance.memory.usedJSHeapSize;
- 此外還有一些高級(jí)用法壁肋,比如可以收集一些請(qǐng)求和靜態(tài)資源的請(qǐng)求時(shí)間
let time = [];
let entryLists = window.performance.getEntries();
for(let i=0;i<entryLists.length;i++) {
let item = entryLists[i];
let obj = {};
let soureTypes = ['script','css','xmlhttprequest','link','img'];
if(soureTypes.indexOf(item.initiatorType)>=0){
obj.name = item.name;
//請(qǐng)求時(shí)間
obj.reqTime = item.responseEnd - item.responseStart;
time.push(obj);
}
}
3.關(guān)于Performance的更多用法可以參考:
http://www.reibang.com/p/1355232d525a
https://blog.csdn.net/hb_zhouyj/article/details/89888646
- 收集數(shù)據(jù)上報(bào)
收集上報(bào)數(shù)據(jù)逮光,使用koa2創(chuàng)建接口performance.gif
const fs = require('fs');
route.get('/performance.gif',async (ctx)=>{
let queryStr = ctx.querystring;
let d = new Date();
let year = d.getFullYear();
let month = d.getMonth()+1;
let day = d.getDate()+1;
fs.writeFile(`../performance-logs/${year}-${month}-${day}.log`,queryStr,{flag:'a',encoding:'utf-8',mode:'0666'},function(e){});
ctx.status = 200;
ctx.type = 'image/gif';
ctx.body = {};
});
3.錯(cuò)誤收集和監(jiān)控
錯(cuò)誤收集主要就是針對(duì)js報(bào)錯(cuò)、靜態(tài)資源加載等出錯(cuò)信息進(jìn)行收集墩划。
- 收集js報(bào)錯(cuò)最先想到的可能是try catch。
try {
console.log(b);
} catch(e) {
console.log(e);
sendErrorReq();
};
但是使用使用的話嗡综,每個(gè)頁面收集錯(cuò)誤都要充斥這try catch乙帮,這樣其實(shí)是不太好的。
而且catch似乎沒辦法捕獲到異步的操作极景。
try {
setTimeout(()=>{
console.log(b);
})
} catch(e) {
console.log(e);
sendErrorReq();
};
試了一下沒有執(zhí)行到catch里邊的sendErrorReq函數(shù)察净。
使用try catch是一種局部錯(cuò)誤監(jiān)聽方式驾茴。
- 用window.onerror或者是window.addEventListener(‘error’)
window.onerror
/**
*msg 錯(cuò)誤信息
*url 錯(cuò)誤所在的頁面地址
*row 錯(cuò)誤所在的行數(shù)
*col 錯(cuò)誤所在的列數(shù)
**/
window.onerror = function(msg,url,row,col) {
console.log(msg,url,row,col, error);
};
b();
使用window.onerror可以檢測(cè)到j(luò)s的報(bào)錯(cuò),但是沒法監(jiān)聽靜態(tài)資源加載失敗的情況氢卡。
<img src="一個(gè)不存在的圖片地址" alt="">
window.addEventListener(‘error’)
window.addEventListener(‘error’)能夠監(jiān)聽到靜態(tài)資源加載出錯(cuò)
window.addEventListener('error',(e)=>{
let localName = e.srcElement.localName;
let currentSrc = e.srcElement.currentSrc;
if(localName=='img') {
console.log(`圖片${currentSrc}加載失敗了`);
sendErrorData(currentSrc);
}
...
// e.preventDefault();
},true);
必須設(shè)為捕獲過程中執(zhí)行锈至,否則依然無法監(jiān)聽。
3. promise錯(cuò)誤的收集
new Promise((resolve,reject)=>{
reject('hi');
}).catch(e=>{
console.log(e);
sendErrorData(e)
});
axios.get(...).catch(e=>{
console.log(e);
sendErrorData(e)
})
但是有種情況译秦,如果promise不加catch的話峡捡,
沒法通過window.onerror去監(jiān)聽,但是還是通過監(jiān)聽unhandledrejection事件去收集的
4.unhandledrejection
window.addEventListener('unhandledrejection', event => {
console.log('error:'+event.reason);
sendErrorData(event.reason);
});
new Promise((resolve,reject)=>{
reject('hi');
});
在看下在vue中收集報(bào)錯(cuò)筑悴,以vue-cli3創(chuàng)建的項(xiàng)目進(jìn)行演示
測(cè)試了一下window.onerror這種方式 無法監(jiān)聽錯(cuò)誤的们拙。
在網(wǎng)上找了下原因
可以看到在vue的源碼里,因?yàn)槿绻麤]有定義errorHandler就會(huì)走到logError這個(gè)方法阁吝,所以沒法使用window.onerror進(jìn)行監(jiān)聽砚婆。
5.errorHandler
在vue的手冊(cè)中,推薦監(jiān)聽vue報(bào)錯(cuò)的可以使用errorHandler這個(gè)配置方法突勇。
//main.js
Vue.config.errorHandler = function (err, vm, info) {
console.log('錯(cuò)誤是:', err)
sendErrorData(err);
}
然后隨便故意寫錯(cuò)
mounted() {
a();
}
就能收集到報(bào)錯(cuò)信息了
當(dāng)然最后把收集的錯(cuò)誤上報(bào)給服務(wù)器装盯,創(chuàng)建一個(gè)接口error.gif。
const fs = require('fs');
route.get('/error.gif',async (ctx)=>{
let queryStr = ctx.querystring;
....
fs.writeFile(`../error-logs/${year}-${month}-${day}.log`,queryStr,{flag:'a',encoding:'utf-8',mode:'0666'},function(e){});
ctx.status = 200;
ctx.type = 'image/gif';
...
});
- 遺留問題:如何從souremap中收集js的錯(cuò)誤信息甲馋。