![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SOwkjM2kTNldjNlFDMjFTOlNWZxcTN2ETNidTZ0IzNw8CXxAzLcdDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL1M3Lc9CX6MHc0RHaiojIsJye.png)
前言
這幾天心血來潮,想了解一下前端監控的相關知識,可是在檢視了很多資料之後,發現沒有詳細介紹前端監控的相關文章,都是講個大概,反倒是現成的前端監控工具有不少。
為了深入學習前端監控的相關技術原理,這幾天都在查閱相關的資料。現在打算寫一篇文章詳細介紹一下前端監控,對這幾天的研究做一個總結(于是就有了本文)。
// 前端監控流程
資料采集 --> 資料上報 --> 服務端處理 --> 資料庫存儲 --> 資料監控可視化平台
不過,本文隻講監控中的資料采集和資料上報兩個步驟,後續流程需讀者自行研究探索(這也是一種樂趣)。
資料采集
性能資料采集
先來了解一下 Web API window.performance 。
★
Performance 接口可以擷取到目前頁面中與性能相關的資訊,它是 High Resolution Time API 的一部分,同時也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。
”
這個 API 的屬性 timing,包含了頁面加載各個階段的起始及結束時間。
為了友善大家了解 timing 各個屬性的意義,我在知乎找到一位網友對于 timing 寫的簡介,在此轉載一下。
timing: {
// 同一個浏覽器上一個頁面解除安裝(unload)結束時的時間戳。如果沒有上一個頁面,這個值會和fetchStart相同。
navigationStart: 1543806782096,
// 上一個頁面unload事件抛出時的時間戳。如果沒有上一個頁面,這個值會傳回0。
unloadEventStart: 1543806782523,
// 和 unloadEventStart 相對應,unload事件處理完成時的時間戳。如果沒有上一個頁面,這個值會傳回0。
unloadEventEnd: 1543806782523,
// 第一個HTTP重定向開始時的時間戳。如果沒有重定向,或者重定向中的一個不同源,這個值會傳回0。
redirectStart: 0,
// 最後一個HTTP重定向完成時(也就是說是HTTP響應的最後一個比特直接被收到的時間)的時間戳。
// 如果沒有重定向,或者重定向中的一個不同源,這個值會傳回0.
redirectEnd: 0,
// 浏覽器準備好使用HTTP請求來擷取(fetch)文檔的時間戳。這個時間點會在檢查任何應用緩存之前。
fetchStart: 1543806782096,
// DNS 域名查詢開始的UNIX時間戳。
//如果使用了持續連接配接(persistent connection),或者這個資訊存儲到了緩存或者本地資源上,這個值将和fetchStart一緻。
domainLookupStart: 1543806782096,
// DNS 域名查詢完成的時間.
//如果使用了本地緩存(即無 DNS 查詢)或持久連接配接,則與 fetchStart 值相等
domainLookupEnd: 1543806782096,
// HTTP(TCP) 域名查詢結束的時間戳。
//如果使用了持續連接配接(persistent connection),或者這個資訊存儲到了緩存或者本地資源上,這個值将和 fetchStart一緻。
connectStart: 1543806782099,
// HTTP(TCP) 傳回浏覽器與伺服器之間的連接配接建立時的時間戳。
// 如果建立的是持久連接配接,則傳回值等同于fetchStart屬性的值。連接配接建立指的是所有握手和認證過程全部結束。
connectEnd: 1543806782227,
// HTTPS 傳回浏覽器與伺服器開始安全連結的握手時的時間戳。如果目前網頁不要求安全連接配接,則傳回0。
secureConnectionStart: 1543806782162,
// 傳回浏覽器向伺服器發出HTTP請求時(或開始讀取本地緩存時)的時間戳。
requestStart: 1543806782241,
// 傳回浏覽器從伺服器收到(或從本地緩存讀取)第一個位元組時的時間戳。
//如果傳輸層在開始請求之後失敗并且連接配接被重開,該屬性将會被數制成新的請求的相對應的發起時間。
responseStart: 1543806782516,
// 傳回浏覽器從伺服器收到(或從本地緩存讀取,或從本地資源讀取)最後一個位元組時
//(如果在此之前HTTP連接配接已經關閉,則傳回關閉時)的時間戳。
responseEnd: 1543806782537,
// 目前網頁DOM結構開始解析時(即Document.readyState屬性變為“loading”、相應的 readystatechange事件觸發時)的時間戳。
domLoading: 1543806782573,
// 目前網頁DOM結構結束解析、開始加載内嵌資源時(即Document.readyState屬性變為“interactive”、相應的readystatechange事件觸發時)的時間戳。
domInteractive: 1543806783203,
// 當解析器發送DOMContentLoaded 事件,即所有需要被執行的腳本已經被解析時的時間戳。
domContentLoadedEventStart: 1543806783203,
// 當所有需要立即執行的腳本已經被執行(不論執行順序)時的時間戳。
domContentLoadedEventEnd: 1543806783216,
// 目前文檔解析完成,即Document.readyState 變為 'complete'且相對應的readystatechange 被觸發時的時間戳
domComplete: 1543806783796,
// load事件被發送時的時間戳。如果這個事件還未被發送,它的值将會是0。
loadEventStart: 1543806783796,
// 當load事件結束,即加載事件完成時的時間戳。如果這個事件還未被發送,或者尚未完成,它的值将會是0.
loadEventEnd: 1543806783802
}
通過以上資料,我們可以得到幾個有用的時間
// 重定向耗時
redirect: timing.redirectEnd - timing.redirectStart,
// DOM 渲染耗時
dom: timing.domComplete - timing.domLoading,
// 頁面加載耗時
load: timing.loadEventEnd - timing.navigationStart,
// 頁面解除安裝耗時
unload: timing.unloadEventEnd - timing.unloadEventStart,
// 請求耗時
request: timing.responseEnd - timing.requestStart,
// 擷取性能資訊時目前時間
time: new Date().getTime(),
還有一個比較重要的時間就是白屏時間,它指從輸入網址,到頁面開始顯示内容的時間。
将以下腳本放在 </head> 前面就能擷取白屏時間。
<script>
whiteScreen = new Date() - performance.timing.navigationStart
</script>
通過這幾個時間,就可以得知頁面首屏加載性能如何了。
另外,通過 window.performance.getEntriesByType('resource') 這個方法,我們還可以擷取相關資源(js、css、img...)的加載時間,它會傳回頁面目前所加載的所有資源。
它一般包括以下幾個類型
- sciprt
- link
- img
- css
- fetch
- other
- xmlhttprequest
我們隻需用到以下幾個資訊
// 資源的名稱
name: item.name,
// 資源加載耗時
duration: item.duration.toFixed(2),
// 資源大小
size: item.transferSize,
// 資源所用協定
protocol: item.nextHopProtocol,
現在,寫幾行代碼來收集這些資料。
// 收集性能資訊
const getPerformance = () => {
if (!window.performance) return
const timing = window.performance.timing
const performance = {
// 重定向耗時
redirect: timing.redirectEnd - timing.redirectStart,
// 白屏時間
whiteScreen: whiteScreen,
// DOM 渲染耗時
dom: timing.domComplete - timing.domLoading,
// 頁面加載耗時
load: timing.loadEventEnd - timing.navigationStart,
// 頁面解除安裝耗時
unload: timing.unloadEventEnd - timing.unloadEventStart,
// 請求耗時
request: timing.responseEnd - timing.requestStart,
// 擷取性能資訊時目前時間
time: new Date().getTime(),
}
return performance
}
// 擷取資源資訊
const getResources = () => {
if (!window.performance) return
const data = window.performance.getEntriesByType('resource')
const resource = {
xmlhttprequest: [],
css: [],
other: [],
script: [],
img: [],
link: [],
fetch: [],
// 擷取資源資訊時目前時間
time: new Date().getTime(),
}
data.forEach(item => {
const arry = resource[item.initiatorType]
arry && arry.push({
// 資源的名稱
name: item.name,
// 資源加載耗時
duration: item.duration.toFixed(2),
// 資源大小
size: item.transferSize,
// 資源所用協定
protocol: item.nextHopProtocol,
})
})
return resource
}
小結
通過對性能及資源資訊的解讀,我們可以判斷出頁面加載慢有以下幾個原因:
- 資源過多
- 網速過慢
- DOM元素過多
除了使用者網速過慢,我們沒辦法之外,其他兩個原因都是有辦法解決的,性能優化的文章和書籍網上已經有很多了,有興趣可自行查找資料了解。
錯誤資料采集
通過這幾天的資料查找,了解到現在能捕捉的錯誤有三種。
- 資源加載錯誤
- js 執行錯誤
- promise 錯誤
1 通過 addEventListener('error', callback, true) 在捕獲階段捕捉資源加載失敗錯誤。
2 通過 window.onerror 捕捉 js 錯誤。
3 通過 addEventListener('unhandledrejection', callback)捕捉 promise 錯誤,但是沒有發生錯誤的行數,列數等資訊,隻能手動抛出相關錯誤資訊。
我們可以建一個錯誤數組變量 errors 在錯誤發生時,将錯誤的相關資訊添加到數組,然後在某個階段統一上報,具體如何操作請看代碼
// 捕獲資源加載失敗錯誤 js css img...
addEventListener('error', e => {
const target = e.target
if (target != window) {
monitor.errors.push({
type: target.localName,
url: target.src || target.href,
msg: (target.src || target.href) + ' is load error',
// 錯誤發生的時間
time: new Date().getTime(),
})
}
}, true)
// 監聽 js 錯誤
window.onerror = function(msg, url, row, col, error) {
monitor.errors.push({
type: 'javascript',
row: row,
col: col,
msg: error && error.stack? error.stack : msg,
url: url,
// 錯誤發生的時間
time: new Date().getTime(),
})
}
// 監聽 promise 錯誤 缺點是擷取不到行數資料
addEventListener('unhandledrejection', e => {
monitor.errors.push({
type: 'promise',
msg: (e.reason && e.reason.msg) || e.reason || '',
// 錯誤發生的時間
time: new Date().getTime(),
})
})
通過錯誤收集,可以了解到網站錯誤發生的類型及數量,進而可以做相應的調整,以減少錯誤發生。完整代碼和DEMO會在文章末尾放出,大家可以複制代碼(HTML檔案)在本地測試一下。
資料上報
性能資料上報
性能資料可以在頁面加載完之後上報,盡量不要對頁面性能造成影響。
window.onload = () => {
// 在浏覽器空閑時間擷取性能及資源資訊
// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
monitor.performance = getPerformance()
monitor.resources = getResources()
})
} else {
setTimeout(() => {
monitor.performance = getPerformance()
monitor.resources = getResources()
}, 0)
}
}
當然,你也可以設一個定時器,循環上報。不過每次上報最好做一下對比去重再上報,避免同樣的資料重複上報。
錯誤資料上報
我在DEMO裡提供的代碼,是用一個 errors 數組收集所有的錯誤,再在某一階段統一上報(延時上報)。其實,也可以改成在錯誤發生時上報(即時上報)。這樣可以避免在收集完錯誤延時上報還沒觸發,使用者卻已經關掉網頁導緻錯誤資料丢失的問題。
// 監聽 js 錯誤
window.onerror = function(msg, url, row, col, error) {
const data = {
type: 'javascript',
row: row,
col: col,
msg: error && error.stack? error.stack : msg,
url: url,
// 錯誤發生的時間
time: new Date().getTime(),
}
// 即時上報
axios.post({ url: 'xxx', data, })
}
擴充
SPA
window.performance API 是有缺點的,在 SPA 切換路由時,window.performance.timing 的資料不會更新。是以我們需要另想辦法來統計切換路由到加載完成的時間。
拿 Vue 舉例,一個可行的辦法就是切換路由時,在路由的全局前置守衛 beforeEach 裡擷取開始時間,在元件的 mounted 鈎子裡執行 vm.$nextTick 函數來擷取元件的渲染完畢時間。
router.beforeEach((to, from, next) => {
store.commit('setPageLoadedStartTime', new Date())
})
mounted() {
this.$nextTick(() => {
this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)
})
}
除了性能和錯誤監控,其實我們還可以做得更多。
使用者資訊收集
navigator
使用 window.navigator 可以收集到使用者的裝置資訊,作業系統,浏覽器資訊...
UV(Unique visitor)
是指通過網際網路通路、浏覽這個網頁的自然人。通路您網站的一台電腦用戶端為一個訪客。00:00-24:00内相同的用戶端隻被計算一次。一天内同個訪客多次通路僅計算一個UV。在使用者通路網站時,可以生成一個随機字元串+時間日期,儲存在本地。在網頁發生請求時(如果超過當天24小時,則重新生成),把這些參數傳到後端,後端利用這些資訊生成 UV 統計報告。
PV(Page View)
即頁面浏覽量或點選量,使用者每1次對網站中的每個網頁通路均被記錄1個PV。使用者對同一頁面的多次通路,通路量累計,用以衡量網站使用者通路的網頁數量。
頁面停留時間
傳統網站使用者在進入 A 頁面時,通過背景請求把使用者進入頁面的時間捎上。過了 10 分鐘,使用者進入 B 頁面,這時背景可以通過接口捎帶的參數可以判斷出使用者在 A 頁面停留了 10 分鐘。SPA可以利用 router 來擷取使用者停留時間,拿 Vue 舉例,通過 router.beforeEach destroyed 這兩個鈎子函數來擷取使用者停留該路由元件的時間。
浏覽深度
通過 document.documentElement.scrollTop 屬性以及螢幕高度,可以判斷使用者是否浏覽完網站内容。
頁面跳轉來源
通過 document.referrer 屬性,可以知道使用者是從哪個網站跳轉而來。
通過分析使用者資料,我們可以了解到使用者的浏覽習慣、愛好等等資訊,想想真是恐怖,毫無隐私可言。
DEMO
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script>
function monitorInit() {
const monitor = {
// 資料上傳位址
url: '',
// 性能資訊
performance: {},
// 資源資訊
resources: {},
// 錯誤資訊
errors: [],
// 使用者資訊
user: {
// 螢幕寬度
screen: screen.width,
// 螢幕高度
height: screen.height,
// 浏覽器平台
platform: navigator.platform,
// 浏覽器的使用者代理資訊
userAgent: navigator.userAgent,
// 浏覽器使用者界面的語言
language: navigator.language,
},
// 手動添加錯誤
addError(error) {
const obj = {}
const { type, msg, url, row, col } = error
if (type) obj.type = type
if (msg) obj.msg = msg
if (url) obj.url = url
if (row) obj.row = row
if (col) obj.col = col
obj.time = new Date().getTime()
monitor.errors.push(obj)
},
// 重置 monitor 對象
reset() {
window.performance && window.performance.clearResourceTimings()
monitor.performance = getPerformance()
monitor.resources = getResources()
monitor.errors = []
},
// 清空 error 資訊
clearError() {
monitor.errors = []
},
// 上傳監控資料
upload() {
// 自定義上傳
// axios.post({
// url: monitor.url,
// data: {
// performance,
// resources,
// errors,
// user,
// }
// })
},
// 設定資料上傳位址
setURL(url) {
monitor.url = url
},
}
// 擷取性能資訊
const getPerformance = () => {
if (!window.performance) return
const timing = window.performance.timing
const performance = {
// 重定向耗時
redirect: timing.redirectEnd - timing.redirectStart,
// 白屏時間
whiteScreen: whiteScreen,
// DOM 渲染耗時
dom: timing.domComplete - timing.domLoading,
// 頁面加載耗時
load: timing.loadEventEnd - timing.navigationStart,
// 頁面解除安裝耗時
unload: timing.unloadEventEnd - timing.unloadEventStart,
// 請求耗時
request: timing.responseEnd - timing.requestStart,
// 擷取性能資訊時目前時間
time: new Date().getTime(),
}
return performance
}
// 擷取資源資訊
const getResources = () => {
if (!window.performance) return
const data = window.performance.getEntriesByType('resource')
const resource = {
xmlhttprequest: [],
css: [],
other: [],
script: [],
img: [],
link: [],
fetch: [],
// 擷取資源資訊時目前時間
time: new Date().getTime(),
}
data.forEach(item => {
const arry = resource[item.initiatorType]
arry && arry.push({
// 資源的名稱
name: item.name,
// 資源加載耗時
duration: item.duration.toFixed(2),
// 資源大小
size: item.transferSize,
// 資源所用協定
protocol: item.nextHopProtocol,
})
})
return resource
}
window.onload = () => {
// 在浏覽器空閑時間擷取性能及資源資訊 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
monitor.performance = getPerformance()
monitor.resources = getResources()
console.log('頁面性能資訊')
console.log(monitor.performance)
console.log('頁面資源資訊')
console.log(monitor.resources)
})
} else {
setTimeout(() => {
monitor.performance = getPerformance()
monitor.resources = getResources()
console.log('頁面性能資訊')
console.log(monitor.performance)
console.log('頁面資源資訊')
console.log(monitor.resources)
}, 0)
}
}
// 捕獲資源加載失敗錯誤 js css img...
addEventListener('error', e => {
const target = e.target
if (target != window) {
monitor.errors.push({
type: target.localName,
url: target.src || target.href,
msg: (target.src || target.href) + ' is load error',
// 錯誤發生的時間
time: new Date().getTime(),
})
console.log('所有的錯誤資訊')
console.log(monitor.errors)
}
}, true)
// 監聽 js 錯誤
window.onerror = function(msg, url, row, col, error) {
monitor.errors.push({
type: 'javascript', // 錯誤類型
row: row, // 發生錯誤時的代碼行數
col: col, // 發生錯誤時的代碼列數
msg: error && error.stack? error.stack : msg, // 錯誤資訊
url: url, // 錯誤檔案
time: new Date().getTime(), // 錯誤發生的時間
})
console.log('所有的錯誤資訊')
console.log(monitor.errors)
}
// 監聽 promise 錯誤 缺點是擷取不到行數資料
addEventListener('unhandledrejection', e => {
monitor.errors.push({
type: 'promise',
msg: (e.reason && e.reason.msg) || e.reason || '',
// 錯誤發生的時間
time: new Date().getTime(),
})
console.log('所有的錯誤資訊')
console.log(monitor.errors)
})
return monitor
}
const monitor = monitorInit()
</script>
<link rel="stylesheet" href="test.css">
<title>Document</title>
</head>
<body>
<button class="btn1">錯誤測試按鈕1</button>
<button class="btn2">錯誤測試按鈕2</button>
<button class="btn3">錯誤測試按鈕3</button>
<img src="https://avatars3.githubusercontent.com/u/22117876?s=460&v=4" alt="">
<img src="test.png" alt="">
<script src="192.168.10.15/test.js"></script>
<script>
document.querySelector('.btn1').onclick = () => {
setTimeout(() => {
console.log(button)
}, 0)
}
document.querySelector('.btn2').onclick = () => {
new Promise((resolve, reject) => {
reject({
msg: 'test.js promise is error'
})
})
}
document.querySelector('.btn3').onclick = () => {
throw ('這是一個手動扔出的錯誤')
}
</script>
</body>
</html>
參考資料
https://fex.baidu.com/blog/2014/05/build-performance-monitor-in-7-days/