
前端監(jiān)控細(xì)分可為倆大類,技術(shù)監(jiān)控和行為監(jiān)控。
技術(shù)準(zhǔn)備
此處目的是生成使用的sdk,sdk打包更適合rollup
- npm創(chuàng)建項(xiàng)目
- 安裝依賴
"devDependencies": {
"@babel/core": "^7.11.1",
"babel": "^6.23.0",
"cross-env": "^7.0.2",
"rollup": "^2.26.3",
"rollup-plugin-babel": "^4.4.0"
}
- 配置文件
//rollup.config.js
import babel from "rollup-plugin-babel";
let isDev = (process.env.NODE_ENV === 'develop');
let babaelConfig = {
"presets": [
[
"env", {
"modules": false,
"targets": {
"browers": ["chrome>40", "safari>=7"]
}
}
]
]
}
export default {
input: 'index.js',
watch: {
exclude: 'node_modules/**'
},
output: {
file: isDev ? '../website/client/js/eagle-monitor/bundle.umd.js' : '../dist/bundle.umd.js',
name: 'EagleMonitor',
format: 'umd',
sourcemap: true
},
plugin: [
babel({
babelrc:false,
presets:babaelConfig.presets,
plugins:babaelConfig.plugin,
exclude:'node_modules/**'
})
]
}
- 打包入口類
//index.js
import perf from "./perf";
import resource from "./resource";
import xhrHook from "./xhrHook";
import beh from "./beh";
import errorCatch from "./errorCatch";
perf.init(perfData=>{
console.log('perf');
})
resource.init(item=>{
console.log('resource');
})
beh.init(value=>{
console.log('beh');
})
errorCatch.init(errorInfo=>{
console.log('errorInfo');
})
xhrHook.init(xhrInfo=>{
console.log('xhrInfo');
})
技術(shù)監(jiān)控
- 頁(yè)面性能監(jiān)控
- performance.timing
export default {
init: cb => {
let isDOMReady=false;
let isOnload=false;
let Util = {
getPerfData: p => {
let data = {
//網(wǎng)絡(luò)建連
prevPage: p.fetchStart - p.navigationStart,//上一個(gè)頁(yè)面的時(shí)間
redirect: p.redirectEnd - p.redirectStart,//重定向時(shí)間
dns: p.domainLookupEnd - p.domainLookupStart,//DNS查找時(shí)間
connect: p.connectEnd - p.connectStart,//TCP建連時(shí)間
network: p.connectEnd - p.navigationStart,//網(wǎng)絡(luò)總耗時(shí)
//網(wǎng)絡(luò)接收
send: p.responseStart - p.requestStart,//前端從發(fā)送到接收的時(shí)間
receive: p.responseEnd - p.responseStart,//接收數(shù)據(jù)用時(shí)
request: p.responseEnd - p.requestStart,//請(qǐng)求頁(yè)面的總耗時(shí)
//前端渲染
dom: p.domComplete - p.domLoading,//dom解析時(shí)間
loadEvent: p.loadEventEnd - p.loadEventStart,//loadEvent時(shí)間
frotend: p.loadEventEnd - p.domLoading,//前端總時(shí)間
//關(guān)鍵階段
load: p.loadEventEnd - p.navigationStart,//頁(yè)面完全加載的時(shí)間
domReady: p.domContentLoadedEventStart - p.navigationStart,//dom準(zhǔn)備時(shí)間
interactive: p.domInteractive - p.navigationStart,//用戶可操作的時(shí)間
ttfb: p.responseStart - p.navigationStart //首字節(jié)時(shí)間
}
return data;
},
//dom解析完成
domready: (callback) => {
if (isDOMReady===true) return
let timer = null;
//之所以這樣做,而不是初始時(shí)候執(zhí)行一次,是因?yàn)楦鱾€(gè)屬性值計(jì)算的時(shí)候,可能還沒(méi)就緒,例如dom相關(guān),很容易出現(xiàn)默認(rèn)值計(jì)算
//然后結(jié)果是復(fù)數(shù),所以需要這要遞歸,直到出現(xiàn)真實(shí)結(jié)果
let runCheck = () => {
if (performance.timing.domInteractive) {
//停止循環(huán)檢測(cè) 然后運(yùn)行callback
clearTimeout(timer);
callback();
isDOMReady=true
} else {
//再去循環(huán)檢測(cè)
timer = setTimeout(runCheck, 100);
}
}
if (document.readyState === 'interactive') {
callback();
return
}
document.addEventListener('DOMContentLoaded', () => {
//開(kāi)始循環(huán)檢測(cè),是否DOMContentLoaded已經(jīng)完成
runCheck();
})
},
//頁(yè)面加載完成
onload: (callback) => {
if (isOnload===true) return
let timer = null;
let runCheck = () => {
if (performance.timing.loadEventEnd) {
//停止循環(huán)檢測(cè) 然后運(yùn)行callback
clearTimeout(timer);
callback();
isOnload=true
} else {
//再去循環(huán)檢測(cè)
timer = setTimeout(runCheck, 100);
}
}
if (document.readyState === 'interactive') {
callback();
return
}
window.addEventListener('load', () => {
//開(kāi)始循環(huán)檢測(cè),是否DOMContentLoaded已經(jīng)完成
runCheck();
})
}
}
let performance = window.performance; //兼容性問(wèn)題,所以此處只是簡(jiǎn)化代碼
Util.domready(() => {
let perfData = Util.getPerfData(performance.timing);
perfData.type='domready';
cb(perfData);
debugger
})
Util.onload(() => {
let perfData = Util.getPerfData(performance.timing);
perfData.type='onload';
cb(perfData);
debugger
})
// window.addEventListener('load', () => {
// setTimeout(() => {
// console.log(performance.timing);
// let perfData = Util.getPerfData(performance.timing);
// debugger
// }, 100);
// })
}
}
/**
* performance.timing
*
*
含義 默認(rèn)值
connectEnd: 1597806127336 向服務(wù)器建立連接結(jié)束 fetchStart
connectStart: 1597806127335 向服務(wù)器建立連接開(kāi)始 fetchStart
domComplete: 0 文檔解析完成
domContentLoadedEventEnd: 0 ContentLoaded結(jié)束
domContentLoadedEventStart: 0 ContentLoaded開(kāi)始 備注(只針對(duì)dom結(jié)構(gòu),而不針對(duì)里面圖片等資源)
domInteractive: 0 解析dom結(jié)束 備注:document.readyState為字符串interactive
domLoading: 1597806127367 解析dom開(kāi)始 備注:document.readyState為字符串loading
domainLookupEnd: 1597806127335 dns查詢結(jié)束 fetchStart
domainLookupStart: 1597806127335 dns查詢開(kāi)始 fetchStart
fetchStart: 1597806127332 開(kāi)始請(qǐng)求網(wǎng)頁(yè)
loadEventEnd: 0 load事件發(fā)送后
loadEventStart: 0 load事件發(fā)送前
navigationStart: 1597806127331 前一個(gè)網(wǎng)頁(yè)卸載的時(shí)間 fetchStart
redirectEnd: 0 重定向結(jié)束時(shí)間 0 需要同域
redirectStart: 0 重定向開(kāi)始時(shí)間 0 需要同域
requestStart: 1597806127336 向服務(wù)器發(fā)送請(qǐng)求開(kāi)始 無(wú)默認(rèn)值
responseEnd: 1597806127361 服務(wù)器返回?cái)?shù)據(jù)結(jié)束
responseStart: 1597806127360 服務(wù)器返回?cái)?shù)據(jù)開(kāi)始
secureConnectionStart: 0 安全握手開(kāi)始 0 非https的沒(méi)有
unloadEventEnd: 1597806127365 前一個(gè)網(wǎng)頁(yè)的unload(關(guān)掉)時(shí)間結(jié)束 0
unloadEventStart: 1597806127365 前一個(gè)網(wǎng)頁(yè)的unload(關(guān)掉)時(shí)間開(kāi)始 0
*/
總之:注意屬性值的觸發(fā)問(wèn)題(例如初始化執(zhí)行時(shí)候?qū)?yīng)周期還沒(méi)有,值是默認(rèn)值導(dǎo)致計(jì)算出錯(cuò),解決方案是遞歸)
- 靜態(tài)資源性能監(jiān)控
//util.js
export default{
onload:(cb)=>{
if ( document.readyState==='complete') {
cb();
return
}
window.addEventListener('load',()=>{
cb();
})
}
}
import Util from "./util";
let resolvePerformanceResource = (resourceData) => {
let r = resourceData;
let o = {
initiatorType: r.initiatorType,
name: r.name,
duration: parseInt(r.duration),
//連接過(guò)程
redirect: r.redirectEnd - r.redirectStart,//重定向
dns: r.domainLookupEnd - r.domainLookupStart,//DNS查找時(shí)間
connect: r.connectEnd - r.connectStart,//TCP建連時(shí)間
network: r.connectEnd - r.startTime,//網(wǎng)絡(luò)總耗時(shí)
//接收過(guò)程
send: r.responseStart - r.requestStart,//前端從發(fā)送到接收的時(shí)間
receive: r.responseEnd - r.responseStart,//接收數(shù)據(jù)用時(shí)
request: r.responseEnd - r.requestStart,//請(qǐng)求頁(yè)面的總耗時(shí)
//核心指標(biāo)
ttfb: r.responseStart - r.requestStart //首字節(jié)時(shí)間
}
return o;
}
//幫助我們循環(huán)獲得每一個(gè)資源的性能數(shù)據(jù)
let resolveEntries = (entries) => entries.map(_ => resolvePerformanceResource(_));
export default {
init: cb => {
//此處意義是使用新的api,通過(guò)回調(diào)觸發(fā),而不是else里面的什么請(qǐng)求都進(jìn)
if (window.PerformanceObserver) {
//動(dòng)態(tài)獲得每一個(gè)資源信息
let observer = new window.PerformanceObserver((list) => {
try {
let entries = list.getEntries();
} catch (error) {
console.error(error);
}
});
observer.observe({entryTypes: ['resource']})
} else {
Util.onload(() => {
//在onload之后獲得所有的資源信息
let entries = performance.getEntries('resource');
let entriesData = resolveEntries(entries);
cb(entriesData);
// resolvePerformanceResource(entries[0])
debugger
});
}
}
}
總之:監(jiān)控sdk的js盡量放在所有要加載的link,js,css上面,因?yàn)橹挥凶?cè)了才能監(jiān)控,放在下面的話,可能導(dǎo)致監(jiān)控不到之前的請(qǐng)求
- 錯(cuò)誤監(jiān)控
- window.onerror
let formatError=errorObj=>{
debugger
let col=errorObj.column||errorObj.columnNumber;//兼容不同瀏覽器
let row=errorObj.line||errorObj.lineNumber;
let errorType=errorObj.name;
let message=errorObj.message;
let {stack}=errorObj;
if (stack) {
//正則很多,所以理論上應(yīng)該try不然很容i報(bào)錯(cuò)
let matchUrl=stack.match(/https?:\/\/[^\n]+/);
let urlFirstStack=matchUrl?matchUrl[0]:'';
//獲取真正的url
let resourceUrl='';
let regUrlCheck=/https?:\/\/(\S)*\.js/;
if (resourceUrl.test(urlFirstStack)) {
resourceUrl=urlFirstStack.match(regUrlCheck)[0];
}
//獲取真正的行列信息
let stackCol=null;
let stackRow=null;
//chrome只能通過(guò)正則匹配出來(lái)行列
let posStack=urlFirstStack.match(/:(\d+):(\d+)/)
if (posStack&&posStack.length>=3) {
[,stack,stackRow]=posStack;
}
return {
content:stack,
col:Number(col||stackCol),
row:Number(row||stackRow),
errorType,
message,
resourceUrl
};
}
}
export default{
init:cb=>{
let _origin_error=window.onerror;
window.onerror=function(message,source,lineno,colno,error) {
/**
colno: 5 列
error: ReferenceError: b is not defined at http://127.0.0.1:3003/:17:5
lineno: 17 行
message: "Uncaught ReferenceError: b is not defined"
source: "http://127.0.0.1:3003/"
*/
//注意:一般項(xiàng)目都會(huì)壓縮,如果壓縮之后可能是第一行,xxxx列,完全無(wú)法調(diào)試
/**
* 之所以通過(guò)stack去解析正則匹配,就是因?yàn)楸热缯f(shuō)react項(xiàng)目,打包之后,直接lineno,colno出來(lái)的數(shù)據(jù)可能不對(duì)
* 甚至source:定位報(bào)錯(cuò)的js都不對(duì)(例如報(bào)到vendor.js中的錯(cuò)誤,沒(méi)有任何意義),所以這些數(shù)據(jù)只是參考,主要還是上面formatError解析之后的信息,一定是正確的
* 而且這些報(bào)錯(cuò)都是sourcemap的,需要利用服務(wù)端反解,然后返給bug統(tǒng)計(jì)界面,說(shuō)明報(bào)錯(cuò)的代碼是啥
*/
let errorInfo=formatError(error);
errorInfo._message=message;
errorInfo._source=source;
errorInfo._lineno=lineno;
errorInfo._colno=colno;
errorInfo.type='error';
cb(errorInfo);
_origin_error&&_origin_error.apply(window,arguments);
}
}
}
只有這些不夠,因?yàn)獒槍?duì)vue,react這種壓縮類型的項(xiàng)目,真實(shí)運(yùn)行的都是mapresource資源,所以需要服務(wù)端配置,然后解析,最終定位錯(cuò)誤代碼的上下幾行,然后返回給錯(cuò)誤監(jiān)控平臺(tái)顯示
需要注意的是:chrome的報(bào)錯(cuò)信息很多其實(shí)都是無(wú)效數(shù)據(jù),因?yàn)闊o(wú)法精確定位行數(shù)等(例如單頁(yè)面應(yīng)用),所以一般都是解析stack的數(shù)據(jù)正則匹配,這里面取出的才是真實(shí)問(wèn)題所在;還有錯(cuò)誤統(tǒng)計(jì)代碼本身也可能出錯(cuò),這時(shí)候的出錯(cuò)要try處理,同時(shí)做特殊上班,例如分類,但是不能不做任何處理,否則直接回出現(xiàn)死循環(huán)
//服務(wù)端解析source-map
const fs = require('fs');
const path = require('path');
const SourceMap = require('source-map');
let sourceMapFilePath = path.join(__dirname, './main.bundle.js.map');
let sourceFileMap = {};
//替換不規(guī)則路徑,此處是該map文件內(nèi)部文件路徑的替換
let fixPath = filePath => {
return filePath.replace(/\.[\.\/]+/, '');
}
module.exports = async (ctx, next) => {
//一般是把sourcemap文件在客戶端上傳上去,然后再此處再反解,
//但是這只是案例,sourcemap文件直接放到服務(wù)端,只寫(xiě)反解邏輯
if (ctx.path = '/sourcemap') {
let sourceMapContent = fs.readFileSync(sourceMapFilePath, 'utf-8');
let fileObj = JSON.parse(sourceMapContent);
let { sources } = fileObj;
sources.forEach(item => {
sourceFileMap[fixPath(item)] = item;
})
let column = 554;//此處假設(shè)網(wǎng)絡(luò)請(qǐng)求已經(jīng)把報(bào)錯(cuò)位置上傳上來(lái)
let line = 17;
const consumer = await new SourceMap.SourceMapConsumer(sourceMapContent);
let result = consumer.originalPositionFor({
line, column
});
/**
* result
* {
* source:"webpack:///react-app.js",
* line:10
* column:6
* name :vue vue就是錯(cuò)誤,未定義但是使用了
* }
*/
let originSource = sourceFileMap[result.source];
//originSource:"webpack:///./react-app.js",
//報(bào)錯(cuò)的代碼-但是基本上是出錯(cuò)js的全部
let sourceContent=fileObj.sourceContent[sources.indexOf(originSource)];
//可以通過(guò)這個(gè)分行取出所需行數(shù)的上下幾行,甚至標(biāo)紅出錯(cuò)的行
let sourceContentArr=sourceContent.split('\n');
ctx.body = {sourceContent, sourceContentArr,originSource, result };
}
//此處是koa的中間件,所以這么寫(xiě),不需要特別在意
return next();
}
- 接口性能監(jiān)控
核心就是類似代理模式,重寫(xiě)XMLHttpRequest的send和open函數(shù),然后類似于面向切面的形式,在里面實(shí)現(xiàn)信息上報(bào)
export default {
//TODO 自身SDK請(qǐng)求不需要攔截
init: cb => {
//xhr hook
let xhr = window.XMLHttpRequest;
//避免多次加載該hook,例如用戶把sdk引用兩次的情況
if (xhr._eagle_monitor_flag === true) {
return
}
xhr._eagle_monitor_flag = true;
let _originOpen = xhr.prototype.open;
//原生xhr有這幾個(gè)參數(shù)
xhr.prototype.open = function (method, url, async, user, password) {
//此處是面向切面編程
this._eagle_xhr_info = {
url, method, status: null
};
return _originOpen.apply(this, arguments);
}
let _originSend = xhr.prototype.send;
xhr.prototype.send = function (value) {
let _self = this;
this._eagle_start_time = Date.now();
//注意此處,是多個(gè)箭頭的高階函數(shù)
let ajaxEnd = eventType => () => {
if (_self.response) {
let responseSize = null;
switch (_self.responseType) {
case 'json':
responseSize = JSON.stringify(_self.response).length;
break;
case 'arraybuffer':
responseSize = _self.response.byteLength;
break
default:
//注意這里:responseText和response區(qū)別
responseSize = _self.responseText.length;
break
}
_self._eagle_xhr_info.event = eventType;
_self._eagle_xhr_info.status = _self.status;
_self._eagle_xhr_info.success = _self.status === 200;
_self._eagle_xhr_info.duration = Date.now() - _self._eagle_start_time;
_self._eagle_xhr_info.responseSize = responseSize;
_self._eagle_xhr_info.requestSize = value ? value.length : 0;
_self._eagle_xhr_info.type = 'xhr';
cb(_self._eagle_xhr_info);
}
//注意:如果sdk也報(bào)錯(cuò),如果不做任何處理會(huì)被sdk錯(cuò)誤統(tǒng)計(jì)上報(bào),然后上報(bào)繼續(xù)報(bào)錯(cuò),不一會(huì)CPU就是滿,所以sdk錯(cuò)誤需要捕獲,然后特殊處理
};
//這三種狀態(tài)都代表著請(qǐng)求已經(jīng)結(jié)束了,需要統(tǒng)計(jì)一些信息并上報(bào)
this.addEventListener('load', ajaxEnd('load'), false);
this.addEventListener('error', ajaxEnd('error'), false);
this.addEventListener('abort', ajaxEnd('abort'), false); //取消請(qǐng)求
//上面是統(tǒng)計(jì)邏輯,這才是真實(shí)調(diào)用網(wǎng)絡(luò)請(qǐng)求,例如json請(qǐng)求
return _originSend.apply(this, arguments);
}
//ftech hook
if (window.fetch) {
let _origin_fetch = window.fetch;
window.fetch = function () {
let startTime = Date.now();
let args = [].slice.call(arguments);
let fetchInput = args[0];
let method = 'GET';
let url = null;
if (typeof fetchInput === 'string') {
url = fetchInput
} else if ('Request' in window && fetchInput instanceof window.Request) {
url = fetchInput.url;
if (fetchInput.method) {
method = fetchInput.method;
}
} else {
url = '' + fetchInput;
}
let eagleFetchData = {
method, url, status: null
}
return _origin_fetch.apply(this,args).then(function(response){
eagleFetchData.status=response.status;
eagleFetchData.type='fetch';
eagleFetchData.status=response.status;
eagleFetchData.duration = Date.now() - startTime;
cb(eagleFetchData);
return response;
})
}
}
}
}
- 自身sdk請(qǐng)求不需要攔截,否則死循環(huán)
- 此處處理了兩個(gè)一個(gè)是fetch一個(gè)是XMLHttpRequest
行為監(jiān)控
行為監(jiān)控重點(diǎn)只說(shuō)一下用戶行為路徑,其他不做討論,因?yàn)楦鞣N方式不同,其中打點(diǎn)監(jiān)控,例如用戶點(diǎn)擊一下觸發(fā)一次行為監(jiān)控也算,方式很多。
核心是通過(guò)xpath形式,統(tǒng)計(jì)用戶的行為,例如點(diǎn)擊了什么
// /html/body/ul[1]/li[1] xpath
/**
* 當(dāng)然xpath不能直接這么用,因?yàn)楹芏郿om點(diǎn)擊不需要上傳
* 但是比如說(shuō)支付按鈕,則肯定有特殊id或者類名,可以這樣過(guò)濾需要的
*/
//獲取我的元素是兄弟元素的第幾個(gè)
let getIndex=ele=>{
let children=[].slice.call(ele.parentNode.children);
let myIndex=null;
children=children.filter(node=>node.tagName===ele.tagName);
for (let i = 0; i < children.length; i++) {
if (ele===children[i]) {
myIndex=i;
break
}
}
myIndex= `[${myIndex+1}]`;
let tagName=ele.tagName.toLocaleLowerCase();
let myLabel=tagName+myIndex;
return myLabel;
}
let getXpath=ele=>{
let xpath='';
let currentEle=ele;
while (currentEle!==document.body) {
xpath=getIndex(currentEle)+'/'+xpath;
currentEle=currentEle.parentNode;
}
}
export default{
init:cb=>{
document.addEventListener('click',e=>{
let target=e.target;
let xpath=getXpath(target);
},false);
}
}
全局注冊(cè)點(diǎn)擊監(jiān)控,然后通過(guò)遞歸遍歷,查找到具體是點(diǎn)擊什么,形成xpath類似于這種/html/body/ul[1]/li[1],然后可以通過(guò)不同類名或者id去過(guò)濾需要的行為。例如:支付按鈕的樣式肯定不同,而且一般不可能統(tǒng)計(jì)所有行為,因?yàn)閿?shù)據(jù)量太大,如果完全不講究全部上傳也行,那樣就需要后端處理邏輯去過(guò)濾行為,否則行為統(tǒng)計(jì)頁(yè)面數(shù)據(jù)量太多,完全無(wú)法有效觀看。
補(bǔ)充:前端頁(yè)面打點(diǎn)為什么一般要用gif打點(diǎn)
參考-拷貝: https://blog.csdn.net/weixin_37719279/article/details/103476567?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2allfirst_rank_v2~rank_v25-7-103476567.nonecase&utm_term=%E6%89%93%E7%82%B9%E7%9A%84%E6%84%8F%E6%80%9D
所謂的前端監(jiān)控,其實(shí)是在滿足一定條件后,由Web頁(yè)面將用戶信息(UA/鼠標(biāo)點(diǎn)擊位置/頁(yè)面報(bào)錯(cuò)/停留時(shí)長(zhǎng)/etc)上報(bào)給服務(wù)器的過(guò)程。一般是將上報(bào)數(shù)據(jù)用url_encode(百度統(tǒng)計(jì)/CNZZ)或JSON編碼(神策/諸葛io)為字符串,通過(guò)url參數(shù)傳遞給服務(wù)器,然后在服務(wù)器端統(tǒng)一處理。
這套流程的關(guān)鍵在于:
- 能夠收集到用戶信息;
- 能夠?qū)⑹占降臄?shù)據(jù)上報(bào)給服務(wù)器。也就是說(shuō),只要能上報(bào)數(shù)據(jù),無(wú)論是請(qǐng)求GIF文件還是請(qǐng)求js文件或者是調(diào)用頁(yè)面接口,服務(wù)器端其實(shí)并不關(guān)心具體的上報(bào)方式。
向服務(wù)器端上報(bào)數(shù)據(jù),可以通過(guò)請(qǐng)求接口,請(qǐng)求普通文件,或者請(qǐng)求圖片資源的方式進(jìn)行。為什么所有系統(tǒng)都統(tǒng)一使用了請(qǐng)求GIF圖片的方式上報(bào)數(shù)據(jù)呢?
- 首先,為什么不能直接用GET/POST/HEAD請(qǐng)求接口進(jìn)行上報(bào)?
這個(gè)比較容易想到原因。一般而言,打點(diǎn)域名都不是當(dāng)前域名,所以所有的接口請(qǐng)求都會(huì)構(gòu)成跨域。而跨域請(qǐng)求很容易出現(xiàn)由于配置不當(dāng)被瀏覽器攔截并報(bào)錯(cuò),這是不能接受的。所以,直接排除。
- 其次,為什么不能用請(qǐng)求其他的文件資源(js/css/ttf)的方式進(jìn)行上報(bào)?
這和瀏覽器的特性有關(guān)。通常,創(chuàng)建資源節(jié)點(diǎn)后只有將對(duì)象注入到瀏覽器DOM樹(shù)后,瀏覽器才會(huì)實(shí)際發(fā)送資源請(qǐng)求。反復(fù)操作DOM不僅會(huì)引發(fā)性能問(wèn)題,而且載入js/css資源還會(huì)阻塞頁(yè)面渲染,影響用戶體驗(yàn)。
但是圖片請(qǐng)求例外。構(gòu)造圖片打點(diǎn)不僅不用插入DOM,只要在js中new出Image對(duì)象就能發(fā)起請(qǐng)求,而且還沒(méi)有阻塞問(wèn)題,在沒(méi)有js的瀏覽器環(huán)境中也能通過(guò)img標(biāo)簽正常打點(diǎn),這是其他類型的資源請(qǐng)求所做不到的。
- 那還剩下最后一個(gè)問(wèn)題,同樣都是圖片,上報(bào)時(shí)選用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件
首先,1x1像素是最小的合法圖片。而且,因?yàn)槭峭ㄟ^(guò)圖片打點(diǎn),所以圖片最好是透明的,這樣一來(lái)不會(huì)影響頁(yè)面本身展示效果,二者表示圖片透明只要使用一個(gè)二進(jìn)制位標(biāo)記圖片是透明色即可,不用存儲(chǔ)色彩空間數(shù)據(jù),可以節(jié)約體積。因?yàn)樾枰该魃钥梢灾苯优懦齁EPG(BMP32格式可以支持透明色)。
- 然后還剩下BMP、PNG和GIF,但是為什么會(huì)選GIF呢?
因?yàn)轶w積!,gif最小,最小的BMP文件需要74個(gè)字節(jié),PNG需要67個(gè)字節(jié),而合法的GIF,只需要43個(gè)字節(jié)。同樣的響應(yīng),GIF可以比BMP節(jié)約41%的流量,比PNG節(jié)約35%的流量。這樣比較一下,答案就很明顯了。
總結(jié)
前端監(jiān)控使用GIF進(jìn)行上報(bào)主要是因?yàn)椋?/p>
- 沒(méi)有跨域問(wèn)題;
- 不會(huì)阻塞頁(yè)面加載,影響用戶體驗(yàn);
- 在所有圖片中體積最小,相較BMP/PNG,可以節(jié)約41%/35%的網(wǎng)絡(luò)資源。