javascript 實現高仿 growingIO

程式設計師解決師發表於2019-03-04

前言

埋點,是網站分析的一種常用的資料採集方法。我們主要用來採集使用者行為資料(例如頁面訪問路徑,點選了什麼元素)進行資料分析,從而讓運營同學更加合理的安排運營計劃。現在市面上有很多第三方埋點服務商,百度統計,友盟,growingIO 等大家應該都不太陌生,大多情況下大家都只是使用,最近我研究了下 web 埋點,你要不要了解下。

現有埋點三大型別

使用者行為分析是一個大系統,一個典型的資料平臺。由使用者資料採集,使用者行為建模分析,視覺化報表展示幾個模組構成。現有的埋點採集方案可以大致被分為三種,手動埋點,視覺化埋點,無埋點

  1. 手動埋點 手動程式碼埋點比較常見,需要呼叫埋點的業務方在需要採集資料的地方呼叫埋點的方法。優點是流量可控,業務方可以根據需要在任意地點任意場景進行資料採集,採集資訊也完全由業務方來控制。這樣的有點也帶來了一些弊端,需要業務方來寫死方法,如果採集方案變了,業務方也需要重新修改程式碼,重新發布。
  2. 視覺化埋點 可是化埋點是近今年的埋點趨勢,很多大廠自己的資料埋點部門也都開始做這塊。優點是業務方工作量少,缺點則是技術上推廣和實現起來有點難(業務方前端程式碼規範是個大前提)。阿里的活動頁很多都是運營通過視覺化的介面拖拽配置實現,這些活動控制元件元素都帶有唯一標識。通過埋點配置後臺,將元素與要採集事件關聯起來,可以自動生成埋點程式碼嵌入到頁面中。
  3. 無埋點 無埋點則是前端自動採集全部事件,上報埋點資料,由後端來過濾和計算出有用的資料,優點是前端只要載入埋點指令碼。缺點是流量和採集的資料過於龐大,伺服器效能壓力山大,主流的 GrowingIO 就是這種實現方案。

我們暫時放棄視覺化埋點的實現,在 手動埋點無埋點 上進行了嘗試,為了便於描述,下文我會稱採集指令碼為 SDK。

思考幾個問題

埋點開發需要考慮很多內容,貫穿著不輕易動手寫程式碼的原則,我們在開發前先思考下面這幾個問題

  1. 我們要採集什麼內容,進行哪些採集介面的約定
  2. 業務方通過什麼方式來呼叫我們的採集指令碼
  3. 手動埋點:SDK 需要封裝一個方法給業務方進行呼叫,傳參方式業務方可控
  4. 無埋點:考慮到資料量對於伺服器的壓力,我們需要對無埋點進行開關配置,可以配置進行哪些元素進行無埋點採集
  5. 使用者標識:遊客使用者和登入使用者的採集資料怎麼進行區分關聯
  6. 裝置Id:使用者通過瀏覽器來訪問 web 頁面,裝置Id需要儲存在瀏覽器上,同一個使用者訪問不同的業務方網站,裝置Id要保持一樣,怎麼實現
  7. 單頁面應用:現在流行的單頁面應用和普通 web 頁面的資料採集是否有差異
  8. 混合應用:app 與 h5 的混合應用我們要怎麼進行通訊

我們要採集什麼內容,進行哪些採集介面的約定

第一期我們先實現對 PV(即頁面瀏覽量或點選量) 、UV(一天內同個訪客多次訪問) 、點選量、使用者的訪問路徑的基礎指標的採集。精細化分析的流量轉化需要和業務相關,需要和資料分析方做約定,我們預留擴充套件。所以我們的採集介面需要進行以下的約定

{
    "header":{ // HTTP 頭部
        "X-Device-Id":" 550e8400-e29b-41d4-a716-446655440000", //裝置ID,用來區分使用者裝置
        "X-Source-Url":"https://www.baidu.com/", //源地址,關聯使用者的整個操作流程,用於使用者行為路徑分析,例如登入,到首頁,進入商品詳情,退出這一整個完整的路徑
        "X-Current-Url":"", //當前地址,使用者行為發生的頁面
        "X-User-Id":"",//使用者ID,統計登入使用者行為
    },
    "body":[{ // HTTP Body體
        "PageSessionID":"", //頁面標識ID,用來區分頁面事件,例如載入和離開我們會發兩個事件,這個標識可以讓我們知道這個事件是發生在一個頁面上
        "Event":"loaded", //事件型別,區分使用者行為事件
        "PageTitle":  "埋點測試頁",  //頁面標題,直觀看到使用者訪問頁面
        "CurrentTime":  “1517798922201”,  //事件發生的時間
        "ExtraInfo":  {
         }    //擴充套件欄位,對具體業務分析的傳參
    }]
}
複製程式碼

以上就是我們現在約定好了的通用的事件採集的介面,所傳的引數基本上會根據採集事件的不同而發生變化。但是在使用者的整一個訪問行為中,使用者的裝置是不會變化的,如果你想採集裝置資訊可以重新約定一個介面,在整個採集開始之前傳送裝置資訊,這樣可以避免在事件採集介面上重複採集固定資料。

{
    "header":{ // HTTP 頭部
          "X-Device-Id"  :"550e8400-e29b-41d4-a716-446655440000"  ,      //  裝置id
    },
    "body":{ // HTTP Body體
              "DeviceType":  "web" ,   //裝置型別
             "ScreenWide"  :  768 , //  螢幕寬
             "ScreenHigh":  1366 , //  螢幕高
             "Language":    "zh-cn"  //語言
    }
}
複製程式碼

業務方通過什麼方式來呼叫我們的採集指令碼

埋點應該讓呼叫的業務方,儘可能少有工作量,最好是什麼都不用做,?,但是實現起來有點難額。我們採用的方案是讓業務方在程式碼裡通過 script 指令碼來引用我們的 SDK ,業務方只要配置一些需要的引數進行埋點定製(?我們講到過的無埋點的流量控制),然後什麼都不做就可以進行基礎資料的採集。

(function() {
			var collect = document.createElement('script');
			collect.type = 'text/javascript';
			collect.async = true;
			collect.src =  'http://collect.trc.com/index.js';
			var s = document.getElementsByTagName('script')[0];
			s.parentNode.insertBefore(collect, s);
	})();


//使用者自定義要進行無埋點採集的元素,如果不進行無埋點採集,可以不配置
 var _XT = [];
_XT.push(['Target','div']);
複製程式碼

手動埋點:SDK

如果業務方需要採集更多業務定製的資料,可以呼叫我們暴露出的方法進行採集

//自定義事件
sdk.dispatch('customEvent',{extraInfo:'自定義事件的額外資訊'})
複製程式碼

遊客與使用者關聯

我們使用 userId 來做使用者標識,同一個裝置的使用者,從遊客使用者切換到登入使用者,如果我們要把他們關聯起來,需要有一個裝置Id 做關聯

web 裝置Id

使用者通過瀏覽器來訪問 web 頁面,裝置Id需要儲存在瀏覽器上,同一個使用者訪問不同的業務方網站,裝置Id要保持一樣。web 變數儲存,我們第一時間想到的就是 cookie,sessionStorage,localStorage,但是這3種儲存方式都和訪問資源的域名相關。我們總不能每次訪問一個網站就新建一個裝置指紋吧,所以我們需要通過一個方法來跨域共享裝置指紋

我們想到的方案是,通過巢狀 iframe 載入一個靜態頁面,在 iframe 上載入的域名上儲存裝置id,通過跨域共享變數獲取裝置id,共享變數的原理是採用了iframe 的 contentWindow通訊,通過 postMessage 獲取事件狀態,呼叫封裝好的回撥函式進行資料處理具體的實現方式

//web 應用,通過嵌入 iframe 進行跨域 cookie 通訊,設定裝置id,
    collect.setIframe = function () {
        var that = this
        var iframe = document.createElement('iframe')
        iframe.id = "frame",
        iframe.src = 'http://collectiframe.trc.com' // 配置域名代理,目的是讓開發測試生產環境程式碼一致
        iframe.style.display='none' //iframe 設定的目的是用來生成固定的裝置id,不展示
        document.body.appendChild(iframe)

        iframe.onload = function () {
                iframe.contentWindow.postMessage('loaded','*');
        }

        //監聽message事件,iframe 載入完成,獲取裝置id ,進行相關的資料採集
        helper.on(window,"message",function(event){
            that.deviceId = event.data.deviceId

            if(event.data && event.data.type == 'loaded'){
                that.sendDevice(that.getDevice(), that.deviceUrl);
                setTimeout(function () {
                    that.send(that.beforeload)
                    that.send(that.loaded)
                },1000)
            }
        })
    }
複製程式碼

iframe 與 SDK 通訊

function receiveMessageFromIndex ( event ) {
    getDeviceInfo() // 獲取裝置資訊
    var data =  {
            deviceId: _deviceId,
            type:event.data
    }

    event.source.postMessage(data, '*'); // 將裝置資訊傳送給 SDK
}

//監聽message事件
if(window.addEventListener){
        window.addEventListener("message", receiveMessageFromIndex, false);
}else{
        window.attachEvent("onmessage", receiveMessageFromIndex, false)
複製程式碼

如果你想知道可以看我的另一篇部落格 web 瀏覽器指紋跨域共享

單頁面應用:現在流行的單頁面應用和普通 web 頁面的資料採集是否有差異

我們知道單頁面應用都是無重新整理的頁面載入,所以我們在頁面跳轉的處理和我們的普通的頁面會有所不同。單頁面應用的路由外掛運用了 window 自帶的無重新整理修改使用者瀏覽記錄的方法,pushState 和 replaceState。

window 的 history 物件 提供了兩個方法,能夠無重新整理的修改使用者的瀏覽記錄,pushSate,和 replaceState,區別的 pushState 在使用者訪問頁面後面新增一個訪問記錄, replaceState 則是直接替換了當前訪問記錄,所以我們只要改寫 history 的方法,在方法執行前執行我們的採集方法就能實現對單頁面應用的頁面跳轉事件的採集了

 // 改寫思路:拷貝 window 預設的 replaceState 函式,重寫 history.replaceState 在方法裡插入我們的採集行為,在重寫的 replaceState 方法最後呼叫,window 預設的 replaceState 方法

    collect = {}
    collect.onPushStateCallback : function(){}  // 自定義的採集方法

    (function(history){
        var replaceState = history.replaceState;   // 儲存原生 replaceState
        history.replaceState = function(state, param) {     // 改寫 replaceState
           var url = arguments[2];
           if (typeof collect.onPushStateCallback == "function") {
                 collect.onPushStateCallback({state: state, param: param, url: url});   //自定義的採集行為方法
           }
           return replaceState.apply(history, arguments);    // 呼叫原生的 replaceState
        };
     })(window.history);
複製程式碼

這塊介紹起來也比較的複雜,如果你想了解更多,可以看我的另一篇部落格你需要知道的單頁面路由實現原理

混合應用:app 與 h5 的混合應用我們要怎麼進行通訊

現在大部分的應用都不是純原生的應用, app 與 h5 的混合的應用是現在的一種主流。

純 web 資料採集我們考慮到前端儲存資料容易丟失,我們在每一次事件觸發的時候都用採集介面傳輸採集到的資料。考慮到現在很多使用者的手機會有流量管家的軟體監控,如果在 App 中 h5 還是採集到資料就傳輸給服務端,很有可能會讓流量管家檢測到,給使用者報警,從而使得使用者不再信任你的 App , 所以我們在使用者操作的時候將資料傳給 app 端,儲存到 app。使用者切換應用到後臺的時候,通過 app 端的 SDK 打包傳輸到伺服器,我們給 app 提供的方法封裝了一個介面卡

// app 與 h5 混合應用,直接將數資訊發給 app
collect.saveEvent = function (jsonString) {

    collect.dcpDeviceType && setTimeout(function () {
        if(collect.dcpDeviceType=='android'){
            android.saveEvent(jsonString)
        } else {
            window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.nativeBridge.postMessage(jsonString) : window.postBridgeMessage(jsonString)
        }

    },1000)
	}
複製程式碼

實現思路

通過上面幾個問題的思考,我們對埋點的實現大致已經有了一些想法,我們使用思維導圖來還原下我們即將要做的事情,圖片記得放大看哦,太小了可能看不清。

  1. 我們需要暴露給業務方呼叫的方法

暴露方法
2. 我們需要處理的事件型別
監聽事件
3. SDK 的基本實現思路
實現邏輯

我們來看下幾個核心程式碼的實現

工具方法

我們定義了幾個工具方法,提高開發的幸福指數 ?

    var helper = {};

    // 生成一個唯一的標識,pageSessionId (用這個變數來關聯開始載入、載入完成、離開頁面的事件,計算出頁面加菜時間,停留時間)
    helper.uuid = function(){}

    // 元素繫結事件監聽,相容瀏覽器到IE8
    helper.on = function(){}

    //元素移除事件監聽的介面卡函式,相容瀏覽器到IE8
    helper.remove = function(){}

    //將json轉為字串,事件傳輸的引數型別轉化
    helper.changeJSON2Query = function(){}

    //將相對路徑解析成文件全路徑
    helper.normalize = function(){}
複製程式碼

採集邏輯

    var collect = {
        deviceUrl:'http://collect.trc.com/rest/collect/device/h5/v1',
        eventUrl:'http://collect.trc.com/rest/collect/event/h5/v1',
        isuploadUrl:'http://collect.trc.com/rest/collect/isupload/app/v1',
        parmas:{ ExtraInfo:{} },
        device:{}
    };

    //獲取埋點配置
    collect.setParames = function(){}

    //更新訪問路徑及頁面資訊
    collect.updatePageInfo = function(){}

    //獲取事件引數
    collect.getParames = function(){}

    //獲取裝置資訊
    collect.getDevice = function(){}

    //事件採集
    collect.send = function(){}

    //裝置採集
    collect.sendDevice = function(){}

    //判斷才否採集,埋點採集的開關
    collect.isupload = function(){

        1. 判斷是否採集,不採集就登出事件監聽(專案中區分遊客身份和使用者身份的採集情況,這個方法會被判斷兩次)
        2. 採集則判斷是否已經採集過
            a.已經採集過不做任何操作
            b.沒有采集過新增事件監聽
        3. 判斷是 混合應用還是純 web 應用
            a.如果是web 應用,呼叫 collect.setIframe 設定 iframe
            b.如果是混合應用 將開始載入和載入完成事件傳輸給 app
    }

    //點選事件處理函式
    collect.clickHandler = function(){}

    //離開頁面的事件處理函式
    collect.beforeUnloadHandler = function(){}

    //頁面回退事件處理函式
    collect.onPopStateHandler = function(){}

    //系統事件初始化,註冊離開事件,瀏覽器後退事件
    collect.event = function(){}

    //獲取記錄開始載入資料資訊
    collect.getBeforeload = function(){}

    //儲存載入完成,獲取裝置型別,記錄載入完成資訊
    collect.onload = function(){

        1. 判斷cookie是否有存裝置型別資訊,有表示混合應用
        2. 採集載入完成時間等資訊
        3. 呼叫 collect.isupload 判斷是否進行採集
    }

    //web 應用,通過嵌入 iframe 進行跨域 cookie 通訊,設定裝置id
    collect.setIframe = function(){}

    //app 與 h5 混合應用,直接將數資訊發給 app,判斷裝置型別做原生方法介面卡
    collect.saveEvent = function(){}

    //採集自定義事件型別
    collect.dispatch = function(){}

    //將引數 userId 存入sessionStorage
    collect.storeUserId = function(){}

    //採集H5資訊,如果是混合應用,將採集到的資訊傳送給 app 端
    collect.saveEventInfo = function(){}

    //頁面初始化呼叫方法
    collect.init = function(){

        1. 獲取開始載入的採集資訊
        2. 獲取 SDK 配置資訊,裝置資訊
        3. 改寫 history 兩個方法,單頁面應用頁面跳轉前呼叫我們自己的方法
        4. 頁面載入完成,呼叫 collect.onload 方法

    }


    collect.init(); // 初始化

    //暴露給業務方呼叫的方法
    return {
        dispatch:collect.dispatch,
        storeUserId:collect.storeUserId,
    }
複製程式碼

擴充套件

?就是我這段時間研究的成果了,程式碼的篇幅比較長,就不放在部落格裡了,感興趣的同學可以加我微信進行交流,或則在文章下面留言,也歡迎大家給我提意見,幫忙優化 ?。

web 瀏覽器指紋跨域共享

你需要知道的單頁面路由實現原理

資料埋點是什麼?設定埋點的意義是什麼?

資料採集與埋點

美團點評前端無痕埋點實踐

如何清楚易懂的解釋“UV和PV"的定義

相關文章