轉(zhuǎn)轉(zhuǎn)前端自動埋點核心原理揭秘

大廠技術(shù)??高級前端??Node進階
點擊上方?程序員成長指北,關(guān)注公眾號
回復(fù)1,加入高級Node交流群
前言
項目上線幾天后,產(chǎn)品同學(xué)突然向你要一個需求文檔內(nèi)沒有,但很重要的埋點,怎么辦?是不是又到了甩鍋的時間了?為了避免這種情況,且鑒于我司"沒有落地的決策是垃圾"的精神,我們產(chǎn)出了這個自動埋點工具。
調(diào)研
現(xiàn)在業(yè)界有很多數(shù)據(jù)采集的廠商,像 Heap、GrowingIo、神策等。也都提出了可視化埋點、無埋點等方案,其中可視化埋點是需要圈選的,并不能滿足我們的需求。而無埋點方案也分很多種,一種是保存所有用戶點擊的 xpath,然后交由后端進行分析,另一種是分析所有標(biāo)簽,收集所有的控件,當(dāng)用戶點擊到控件后進行發(fā)送。這兩種方案雖然能解決我們一部分問題,但是功能還遠遠不夠,暴露的問題也很多,像:
不能靈活的自定義屬性 傳輸時效差 數(shù)據(jù)可靠性欠佳 服務(wù)器和網(wǎng)絡(luò)傳輸壓力更大 等問題
至此業(yè)界暫時沒有一個完美的方案解決這個問題。所以我們拋棄了上述所有方案,決定實現(xiàn)一套更優(yōu)的方案-自動埋點。
以往大家思想都是從頁面結(jié)構(gòu)出發(fā),而我們是直接基于事件的角度來實現(xiàn)的。下面就來聊一聊我們實現(xiàn)的核心方法
關(guān)鍵技術(shù)
豐富的配置項 事件捕獲 埋點發(fā)送
配置項
一個功能強大的庫,少不了豐富的配置功能。我們的配置項主要分為使用范圍、自定義上報事件、路由配置、頁面類型等等。其中使用范圍,我們控制了發(fā)送埋點是項目級的還是頁面級的。自定義上報事件是用來區(qū)分具體上報曝光還是點擊或者其他埋點的。路由配置支持 hash 和 history 類型。頁面類型主要是用來控制發(fā)送參數(shù)時,參數(shù)組合格式配置等等。具體實現(xiàn)暫不展開說明,也非本文重點,接下來我們說一下這個方案的核心模塊
事件捕獲
首先我們知道,項目中埋點上報大體分為:
曝光 點擊 模塊曝光 關(guān)閉 ...
那我們完全可以捕捉到這些事件,并觸發(fā)埋點就可以達到我們的目的了,接下來我們逐個分析。
曝光埋點
曝光埋點我將其分為三種:
直接進入全新頁面 SPA 項目的頁面跳轉(zhuǎn)后的頁面 當(dāng)前頁面隱藏后再喚出的情況
第一種的解決方案:前期實現(xiàn),認(rèn)為加載了這個 js,就算是曝光了,但是后期因為業(yè)務(wù)復(fù)雜度問題,用戶狀態(tài)繁多,我們將其放置到首個接口請求后進行發(fā)送,這樣就可以讓統(tǒng)計更精細化,也更符合 PM 的預(yù)期。
第二種的解決方案:hash 路由:我們可以通過監(jiān)聽hashchange解決
history 路由:我們對pushState和replaceState等原生事件進行了重寫,直接上代碼
let?that?=?this;
let?reWrite?=?function?(type)?{
??let?real?=?history[type];
??return?function?()?{
????let?realFun?=?real.apply(this,?arguments);
????let?newEvent?=?new?Event(type?+?'AutoLego');
????newEvent.arguments?=?arguments;
????window.dispatchEvent(newEvent);
????return?realFun;
??};
};
history.pushState?=?reWrite('pushState');
history.replaceState?=?reWrite('replaceState');
window.addEventListener('replaceStateAutoLego',?function?(e)?{
??that.skipRouterCommon();
});
window.addEventListener('pushStateAutoLego',?function?(e)?{
??that.skipRouterCommon();
});
在執(zhí)行原生的pushState和replaceState之后調(diào)用我們自定義事件pushStateAutoLego和replaceStateAutoLego,來達到發(fā)送埋點的目的
第三種的解決方案:這個比較簡單,就是監(jiān)聽visibilitychange事件,來發(fā)送埋點
document.addEventListener('visibilitychange',?()?=>?{
??if?(document.visibilityState?===?'visible')?{
????that.showLego();
??}
});
點擊埋點
為了更加精準(zhǔn)的發(fā)送埋點,滿足實時性、自定義屬性、可靠性等特點,這邊采用重寫addEventListener方法并配合當(dāng)前點擊 DOM 節(jié)點的屬性來進行實現(xiàn),,部分代碼邏輯
//?擴展監(jiān)聽事件
Element.prototype.realAddEventListener?=?Element.prototype.addEventListener;
Element.prototype.addEventListener?=?function?(a,?b,?c)?{
??if?(a?===?'click')?{
????this.realAddEventListener(a,?reWriteClick(b),?c);
??}?else?{
????this.realAddEventListener(a,?b,?c);
??}
};
//?擴展移出監(jiān)聽事件
Element.prototype.realRemoveEventListener?=
??Element.prototype.removeEventListener;
Element.prototype.removeEventListener?=?function?(a,?b,?c)?{
??if?(a?===?'click')?{
????this.realRemoveEventListener(a,?reWriteClick(b),?c);
??}?else?{
????this.realRemoveEventListener(a,?b,?c);
??}
};
其中只針對click事件進行了重寫,在執(zhí)行真正的點擊回調(diào)函數(shù)b之前,進行數(shù)據(jù)處理并發(fā)送埋點。這樣就實現(xiàn)了針對真實的點擊事件進行捕捉,避免了收集大量的無效點擊和自定義屬性等問題,也不用做定時發(fā)送這樣的邏輯來影響性能了。
模塊曝光
模塊曝光顧名思義,就是只某個模塊展現(xiàn),就會發(fā)送一個展現(xiàn)埋點。通常應(yīng)用在不同狀態(tài)展示不同模塊的情況。遺憾的是,因為技術(shù)有限,我還無法將其完全自動化,只能做到半自動化(⊙︿⊙),如果大家有更好的方法,希望大家不吝賜教。下邊說一下,我實現(xiàn)的具體方法。我針對要曝光的模塊,在 dom 上做了一個標(biāo)記autolego-,當(dāng) dom 發(fā)生變化時,匹配到這個標(biāo)記的時候,發(fā)送埋點,具體實現(xiàn)如下:
//?代碼略有刪減
let?that?=?this;
var?MutationObserver?=?window.MutationObserver;
function?dfs(item)?{
??if?(item.id.indexOf('autolego-')?>?-1)?{
????that.showElLego(item);
??}
??if?(item.childNodes)?{
????item.childNodes.forEach((childItem)?=>?{
??????dfs(childItem);
????});
??}
}
var?observer?=?new?MutationObserver(function?(mutations,?observer)?{
??mutations.forEach((item)?=>?{
????dfs(item.target);
??});
});
observer.observe(document.body,?{
??subtree:?true,
??childList:?true,
??attributes:?true,
});
從代碼中,大家可以發(fā)現(xiàn),核心點只有兩個,其中一個是利用了MutationObserver方法,另一個是對 DOM 樹進行了深度遍歷。
關(guān)閉
因為瀏覽器的兼容問題的存在,關(guān)閉事件這邊分別采用onpagehide和onbeforeunload對 IOS 和其他系統(tǒng)來區(qū)分實現(xiàn)
if?(!!navigator.userAgent.match(/\(i[^;]+;(?U;)??CPU.+Mac?OS?X/))?{
??window.onpagehide?=?()?=>?{
????this.unLoadLego();
??};
}?else?{
??window.onbeforeunload?=?()?=>?{
????this.unLoadLego();
??};
}
事件捕獲我們暫且說到這里,延伸這個思路,我們還可以做的更多,數(shù)據(jù)更加全面準(zhǔn)確。
發(fā)送
在發(fā)送埋點這里大家需要注意一下,像關(guān)閉埋點,如果頁面關(guān)閉過快,我們的埋點會被 abort 掉,所以這里推薦大家用 ajax 來實現(xiàn),
如果對兼容性要求不高,也可以用navigator.sendBeacon來實現(xiàn),這個方法主要是為了滿足統(tǒng)計和診斷代碼而生的,也希望以后兼容性能越來越好。
if?(navigator.sendBeacon)?{
??var?fd?=?new?FormData();
??fd.append('legoType',?'autoLego');
??navigator.sendBeacon(`https://${urlPath}`,?fd);
}
效果

如圖可見,當(dāng)點擊 banner 的時候,自動發(fā)送了點擊埋點,同時觸發(fā)了關(guān)閉頁面的埋點,以及進入到下個頁面的曝光埋點。與此同時,在 backup 區(qū)域,也帶上了更多信息,數(shù)據(jù)更加的全面精準(zhǔn)。備注:效果是通過我們的可視化埋點平臺演示的,后期有機會我們也會介紹,敬請期待
總結(jié)
通過上面的介紹,相信大家已經(jīng)對 自動埋點 的核心技術(shù)已經(jīng)很清楚了。
其實就是針對原生方法的重寫來實現(xiàn)同步發(fā)送埋點的效果。
相較于市面上的無埋點方案,自動埋點的可控性,可擴展性也是非常有優(yōu)勢的。
關(guān)于自動埋點暫時只寫到這里啦,雖然有些功能還有欠缺,但我相信,不斷迭代擴展是肯定可以替代掉手動埋點的。不對的地方也希望大家多多指正,一起進步~
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
???“分享、點贊、在看” 支持一波??
