前端監(jiān)控

1.jpg

前端監(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;
                })
            }
        }
    }
}
  1. 自身sdk請(qǐng)求不需要攔截,否則死循環(huán)
  2. 此處處理了兩個(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ò)資源。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容