基於指令和混合的前端通用埋點方案

 

https://zhuanlan.zhihu.com/p/27659302html

摘要

本文介紹了一種通用的前端埋點方案的設計和實現,具備適配項目普遍,易於使用,與業務邏輯解耦等優勢,已經在外賣商業平臺進行了一段時間的試用,並取得良好效果。前端

背景

銷售CRM方向是外賣爲銷售人員提供各維度的工具和平臺,以幫助提升銷售人員工做的效率。在銷售CRM方向的PC端,一直沒有對用戶行爲的數據採集(即埋點數據採集),因此對於分析用戶行爲、觀察產品使用情況、制定產品策略等都缺少相關的數據支持。jquery

因此在今年3月份銷售CRM方向決定啓動PC端的各方向的埋點,包括智子、任務制、HES、商機等多個系統。PM整理的埋點個數達到了100多個。git

在埋點的後端方案採用DA的SAK。而在前端方向,這幾個系統有使用jquery+widget的老方案,也有基於Vue的技術棧實現。須要如何埋點?怎樣實現簡單高效的埋點?這是須要咱們解決的問題。github

埋點方案的肯定

業界的埋點方案主要分爲如下三類:後端

  • 代碼埋點:在須要埋點的節點調用接口,攜帶數據上傳。如百度統計等;
  • 可視化埋點:使用可視化工具進行配置化的埋點,即所謂的「無痕埋點」,前端在頁面加載時,能夠讀取配置數據,自動調用接口進行埋點。如開源的Mixpanel;
  • 無埋點:前端自動採集所有事件並上報埋點數據。如國內的神策數據等;

在當時排期緊湊,人力緊缺的狀況下,顯然不容許咱們去開發可視化埋點方案和無埋點方案,因此只能採起代碼埋點方案。瀏覽器


代碼埋點分爲 命令式埋點 與 聲明式埋點 。app

命令式埋點

命令式埋點,顧名思義,開發者須要手動在須要埋點的節點處進行埋點。如點擊按鈕或連接後的回調函數、頁面ready時進行請求的發送。你們確定都很熟悉這樣的代碼:框架

// 頁面加載時發送埋點請求
$(document).ready(function(){
   // ... 這裏存在一些業務邏輯
   sendRequest(params);
});
// 按鈕點擊時發送埋點請求
$('button').click(function(){
   // ... 這裏存在一些業務邏輯
   sendRequest(params);
});

能夠很容易發現,這樣的作法頗有可能會將埋點代碼侵入業務代碼,這使總體業務代碼變得繁瑣,容易出錯,且後續代碼會越發膨脹,難以維護。因此,咱們須要讓埋點的代碼與具體的業務邏輯解耦,即 聲明式埋點 ,從而提升埋點的效率和代碼的可維護性。dom

聲明式埋點

理論上,聲明式埋點只須要關注兩個問題:

  • 須要埋點的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的瀏覽器支持狀況已經比較好了.

&amp;lt;img src="https://pic4.zhimg.com/v2-1419467a7f6369f269fe3977f097bcc2_b.jpg" data-rawwidth="1490" data-rawheight="152" class="origin_image zh-lightbox-thumb" width="1490" data-original="https://pic4.zhimg.com/v2-1419467a7f6369f269fe3977f097bcc2_r.jpg"&amp;gt;

但爲了保證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同窗的開發反饋、實際產出數據的結果來看都達到咱們的預期,後續會繼續在一些業務上進行持續迭代和優化。

相關文章
相關標籤/搜索