https://zhuanlan.zhihu.com/p/27659302
摘要
本文介紹了一種通用的前端埋點方案的設計和實現,具有適配項目廣泛,易於使用,與業務邏輯解耦等優點,已經在外賣商業平台進行了一段時間的試用,並取得良好效果。
背景
銷售CRM方向是外賣為銷售人員提供各維度的工具和平台,以幫助提高銷售人員工作的效率。在銷售CRM方向的PC端,一直沒有對用戶行為的數據采集(即埋點數據采集),所以對於分析用戶行為、觀察產品使用狀況、制定產品策略等都缺乏相關的數據支持。
所以在今年3月份銷售CRM方向決定啟動PC端的各方向的埋點,包括智子、任務制、HES、商機等多個系統。PM整理的埋點個數達到了100多個。
在埋點的后端方案采用DA的SAK。而在前端方向,這幾個系統有使用jquery+widget的老方案,也有基於Vue的技術棧實現。需要如何埋點?怎樣實現簡單高效的埋點?這是需要我們解決的問題。
埋點方案的確定
業界的埋點方案主要分為以下三類:
- 代碼埋點:在需要埋點的節點調用接口,攜帶數據上傳。如百度統計等;
- 可視化埋點:使用可視化工具進行配置化的埋點,即所謂的「無痕埋點」,前端在頁面加載時,可以讀取配置數據,自動調用接口進行埋點。如開源的Mixpanel;
- 無埋點:前端自動采集全部事件並上報埋點數據。如國內的神策數據等;
在當時排期緊湊,人力緊缺的情況下,顯然不允許我們去開發可視化埋點方案和無埋點方案,所以只能采取代碼埋點方案。
代碼埋點分為 命令式埋點 與 聲明式埋點 。
命令式埋點
命令式埋點,顧名思義,開發者需要手動在需要埋點的節點處進行埋點。如點擊按鈕或鏈接后的回調函數、頁面ready時進行請求的發送。大家肯定都很熟悉這樣的代碼:
// 頁面加載時發送埋點請求
$(document).ready(function(){
// ... 這里存在一些業務邏輯
sendRequest(params);
});
// 按鈕點擊時發送埋點請求
$('button').click(function(){
// ... 這里存在一些業務邏輯
sendRequest(params);
});
可以很容易發現,這樣的做法很有可能會將埋點代碼侵入業務代碼,這使整體業務代碼變得繁瑣,容易出錯,且后續代碼會愈加膨脹,難以維護。所以,我們需要讓埋點的代碼與具體的業務邏輯解耦,即 聲明式埋點 ,從而提高埋點的效率和代碼的可維護性。
聲明式埋點
理論上,聲明式埋點只需要關注兩個問題:
- 需要埋點的DOM節點;
- 所需攜帶的數據
因此,可以很快想出一個聲明式埋點的方法:
// key表示埋點的唯一標識;act表示埋點方式 <button data-stat="{key:'111', act: 'click'}">埋點</button>
那么可以去遍歷DOM樹,找到 [data-stat] 的節點,給這個button綁上click事件,把這些參數在回調函數中通過請求發出去。
在DOM節點(html)上聲明埋點,與業務邏輯(通常在Javascript文件中)就解耦了。調用也很方便。
看起來很美,但這樣就能解決問題了嗎?顯然是不夠的。還需要解決以下問題:
- 遍歷DOM樹的時機問題,一個簡單的例子,一個表格的行數據是通過異步加載,而表格行中的操作按鈕需要埋點,那么在DOM ready的時候去遍歷,顯然是無法找到的
- 綁定埋點事件次數的問題,怎樣保證埋點事件不會被重復綁定到元素上,一次操作發了N個埋點請求?
- 如何處理特有的埋點行為,如頁面展現埋點,區域展現埋點?
- 如何在解綁時,銷毀已綁定的事件?
通用的解決方案
回顧一下,我們需要解決的問題是:
- 通過聲明式埋點來解耦業務代碼
- 埋點方案需要兼容Vue應用和jquery應用(甚至所有應用)
- 需要支持頁面展現埋點、區域展現埋點、點擊埋點等多種埋點方式
- 極端情況下需要支持命令式埋點
我們最終提出了一個基於Vue指令(Directive)和混合(Mixin)的解決方案:
基於Vue指令的聲明式埋點
由於在埋點的需求中有部分項目使用了Vue作為基礎框架,結合上面聲明式埋點的例子,很容易就聯想到 Vue自定義指令。Vue自定義指令提供了一種機制,將數據的變化映射為 DOM 行為。以 Vue 1.x 版本為例,自定義指令提供了幾個鈎子函數:
- bind:只調用一次,在指令第一次綁定到元素上時調用。
- update: 在 bind 之后立即以初始值為參數第一次調用,之后每當綁定值變化時調用,參數為新值與舊值
- unbind:只調用一次,在指令從元素上解綁時調用
這樣的特性可以很好的解決以上的一些問題。我們只需要像這樣:
Vue.directive('stat', { bind: function () { // 准備工作 }, update: function (newValue, oldValue) { // 值更新時的工作 // 也會以初始值為參數調用一次, 此時可以根據傳值類型來進行相應埋點行為的請求處理 }, unbind: function () { // 清理工作 } })
在一個Vue應用中,不需要再去遍歷DOM樹,因為在Vue應用中基本所有DOM操作都是使用數據的變更結合Vue的內置指令實現,Vue可以感知到這些變更。在指令從元素上解綁時我們也可以去銷毀已經綁定的事件。
那么接下來的問題是,還有一些項目基於 jquery + widget 的老方案實現,那么在這些項目中的DOM操作是jquery甚至原生DOM API來實現,Vue的自定義指令就無法工作。舉個例子:
<div id="container"> <button id="btn">click</button> </div> <script> new Vue({ el: '#container', directives: {stat} }) $('#btn').click(function() { $('#container').append('<button v-stat="{key: '3', act: 'click'}">click</button>') }) </script>
在上面例子中,雖然Vue已經掛載到 container 容器上,引入了自定義指令stat, #btn 這個按鈕點擊時插入了一段帶有指令v-stat的按鈕,因為Vue無法感知這個DOM變更,所以該指令不能被解析。這樣的方式就會失效。
之前在外賣運營平台方向有基於 jquery 的DOM劫持操作的實現,在所有DOM操作中加入埋點相關的邏輯;因為無法保證所有的DOM操作都使用 jquery , 且不能保證所有埋點邏輯完全一致,所以也無法通用。
那么,怎樣保證在任意庫,包括原生API的DOM操作下都感知到DOM的變更並且通知Vue重新解析指令呢?這里就需要引入 MutationObserver。
基於MutationObserver API的Mixin
MutationObserver是在DOM3標准中提出的標准API,提供讓開發者感知到在某一個DOM節點變更的能力。可以監聽以下場景:
- childList: 目標節點的子節點插入刪除引起的變更
- attributes: 目標節點屬性改變引起的變更
- characterData: 目標節點的文本節點改變引起的變更,如通過appendData()等
- subtree: 目標節點的子孫節點改變引起的變更
- attributeOldValue:當attribute監聽被設定為true時,可以記錄改變前的屬性值
- characterDataOldValue:當characterData監聽被設定為true時,可以記錄改變前的屬性值
- attributeFilter:可以設定需要監聽的屬性列表
MutationObserver的瀏覽器支持情況已經比較好了.
但為了保證MutationObserver可以在所有瀏覽器上正常工作,我們仍然引入了這個API的polyfill,詳情可見這里。
在此能力的前提下,我們就可以在任意的DOM操作下觸發Vue進行重新解析指令。
我們將 MutationObserver 封裝進一個 Vue mixin , 非Vue應用的業務代碼只需要引入這個mixin,這樣也可以很好地解耦。
詳細的實現原理可以見以下偽代碼:
let observer; export default { ready() { // 開啟監聽 observer = new MutationObserver(mutations => { this.$compile(this.$el); }); observer.observe(this.$el, config); }, destroyed() { // 清理工作 observer.disconnect(); observer.takeRecords(); } }
關於MutationObserver的詳細介紹請見 標准文獻。
埋點行為的處理
埋點庫另一部分主要的邏輯是處理埋點行為。
頁面展現埋點(ready)
Ready事件的處理,在頁面根元素綁定指令后,在指令第一次update鈎子調用時即可認為該元素ready, 直接發起請求埋點即可;
點擊統計埋點(click)
click事件的處理,在該節點上綁定click事件,在指令解綁時銷毀該事件。
區域展現埋點(show)
區域展現埋點即:當區域為可見狀態變更時進行埋點。
那么,我們同樣需要監聽節點的可見狀態變更。
理論上,DOM可見狀態的變更也在MutationObserver的監聽范圍內,最初的一種思路是:
- 先設定MutationObserver的配置,開啟attributeFilter和attributeOldValue,監聽style的改變
- 看oldValue的值是否包含display: none, 和新狀態比對
- 如成立,發送埋點
let observer = new MutationObserver((mutations) => { if (mutations[0].oldValue.indexOf('display: none') > -1 && mutations[0].target.style.display !== 'none') { sendRequest(); } }) let config = { attributes: true, attributeOldValue: true, attributeFilter: ['style'] }; observer.observe(el, config);
但是這種思路很快被否決,因為很顯然,可見狀態還有可能是被節點類名class控制的。而具體節點上的類名是無法預期的,因此這種方案行不通。
最終我們使用了開源庫 VisSense。VisSense提供了監聽可見狀態變更的能力,具體請見這里,本文不進行詳細描述。
VisSense 實際使用了消息訂閱模式和setInterval來進行周期性的節點狀態檢查,感興趣的同學可以看看它的源碼。
於是在這里我們就可以進行很方便的可見狀態監聽:
function handleShow(el) { var visMonitor = VisSense(el).monitor({ visible: function() { sendRequest(); } }); visMonitor.start(); }
眼球曝光埋點(collect)
眼球曝光埋點標識用戶是否「看到」了某個區域,那么用前端的方式來解釋就是:
- 該區域是可見狀態
- 用戶頁面的滾動條位置與該區域的實際位置相匹配
主要的實現思路就是監聽scroll事件,與當前節點的scrollTop進行對比。
由於本次需求未涉及眼球曝光,本部分不再贅述。
極端情況下的命令式埋點支持
上面的聲明式埋點方案已經可以解決大多數問題。
但是,不是100%的情況都適用聲明式埋點,主要發生在 DOM操作不受開發者完全控制 的情況。
舉個例子,在使用百度地圖API時,在地圖上打一些POI點(markPoint), 或者一些蒙層(如Polygon), 再在點擊這些覆蓋物時埋點,由於這些DOM操作是百度地圖API完成的,無法預期插入了哪些DOM,自然就不能在這些DOM上插入指令。所以只能在調用API時進行命令式埋點。需要我們也提供命令式埋點支持。
命令式埋點的大部分邏輯實際已經包含在指令中,於是我們在指令中提供了這樣的接口方式:
export default { bind() {...}, update() {...}, unbind() {...}, sendStat(val) { // 命令式埋點接口 } }
引入此模塊后,即可以當作Vue指令使用,也可以當做一個API來使用。
其他的一些處理
此外,埋點方案還提供了可配置能力,可以設定測試環境還是生產環境的規則(根據URL匹配),設定埋點請求的URL地址,是否開啟debug模式等。
在測試環境下,埋點請求的時機只會在瀏覽器中進行console.log並打印出觸發埋點的節點,不會實際發送請求,可以支持測試環境下的正常開發,又可以避免埋點出現臟數據。
使用方式
- 在Vue項目中,直接使用自定義指令即可
- 在非Vue項目中,需要引入Mixin。如下
new Vue({ el: '#app', // 根節點 directives: {stat}, mixins: [observerMixin] // 非Vue項目需要引入 })
然后在頁面相應節點進行聲明式埋點即可:
<div id="app" v-stat="{'act':'ready',' key':'samplepg'}"></div> // 頁面展現埋點
<button v-stat="{'act':'click', 'key':'samplebtn'}"></button> // 點擊統計埋點
<div id="container" v-stat="{'act':'show',' key':'samplepn'}"></div> // 區域展現埋點
這樣的埋點方式十分簡便快捷。
實際的使用情況
在實際業務開發過程中,本埋點方案平滑適配了Vue項目和jquery等開發的一些老項目,可以很好地和業務代碼解耦,只需要在需要埋點的DOM節點上進行聲明式埋點,開發簡單高效,在排期人力緊張的情況下,很好地支持了100余個埋點數據統計。
總結
前端的數據采集和上報是構建數據平台的重要環節,而前端如何進行埋點也是值得深究的。為了快速滿足業務的大量埋點需求,我們使用了本文的埋點方案,而且已經大量在商業平台部開發中使用,無論從FE同學的開發反饋、實際產出數據的結果來看都達到我們的預期,后續會繼續在一些業務上進行持續迭代和優化。