前端曝光資料埋點——Intersection Observer+vue指令

amandakelake發表於2019-04-01

一、背景介紹

在電商產品中(可以開啟你的淘寶、天貓、京東App),通過對商品的曝光進行資料埋點,就能反推出使用者的行為和互動習慣,從而優化推薦和搜尋演算法以及互動,最終的目的當然是為了增加使用者購買力。 

曝光:商品出現在使用者眼前,也就是瀏覽器視窗,可謂之曝光

最直白的兩種方法 

這兩種辦法都能用,但是getBoundingClientRect這個API是會引起頁面迴流的,使用不當容易導致效能問題。 

基於此,瀏覽器特意為我們打造了一個[Intersection Observer API - Web API | MDN],把效能相關的細節都處理掉,讓開發者只關心業務邏輯即可

前端曝光資料埋點——Intersection Observer+vue指令


二、Intersection Observer API 

整個曝光打點方案基於[Intersection Observer API - Web API 介面參考 | MDN] 

這個API還比較新,至於相容性問題可用[IntersectionObserver/polyfill· w3c]解決,本質上也是用getBoundingClientRect計算位置,具體怎麼實現可以去看看它的原始碼


三、資料埋點思路

  1. new IntersectionObserver() 例項化一個全域性_observer,每個商品DOM自行把自己加入_observer的觀察列表 (這裡會用Vue的指令來實現) 
  2. 當某個商品DOM進入視窗,收集該商品的資訊,存進一個全域性陣列dotArr中,然後取消對該商品DOM的觀察 
  3. dotArr中取資料進行打點比較簡單 
  •  跑定時器,每隔N秒檢查一次,如果dotArr有資料,就直接上報; 
  • 如果N秒內,dotArr的資料量大於某個量maxNum,不等定時器,直接全部上報
  • 打點不難,難的是不漏以及不重複上報資料,使用者離開頁面前的邊界資料處理
      • 瀏覽器環境:dotArr同時存一份在localStorage中,同步更新資料(增加或者上報完後清空),如果使用者真的在N秒的間隔內,而資料又不夠最大上報量maxNum就離開了頁面,那麼這批資料就等使用者下次再進頁面時,直接從localStorage中取出來打掉,當然如果這個使用者再也不進頁面或者清空了瀏覽器快取,這一點點資料丟失可以接受。 
      • 客戶端webview環境:註冊webview關閉的生命鉤子事件(需要客戶端童鞋支援),離開前全部打掉


    四、程式碼實現

    1、封裝Exposure類

    // polyfill
    import 'intersection-observer';
    // 自行封裝資料上報方法,其實就是網路請求
    import { DotData } from './DotData'
    
    // 可以把節流的時間調大一點,預設是100ms
    IntersectionObserver.prototype['THROTTLE_TIMEOUT'] = 300;
    
    export default class Exposure {
      dotDataArr: Array<string>;
      maxNum: number;
      // _observer可以理解為觀察者的集合吧
      _observer;
      _timer: number;
    
      constructor(maxNum = 20) {
        // 當前收集的  尚未上報的資料  也就是已經進入視窗的DOM節點的資料
        this.dotDataArr = [];
        this.maxNum = maxNum;
        this._timer = 0;
        // 全域性只會例項化一次Exposure類,init方法也只會執行一次
        this.init();
      }
    
      init() {
        const self = this;
        // init只會執行一次,所以這兩邊界處理方法放這就行
        // 把瀏覽器localStorage裡面的剩餘資料打完
        this.dotFromLocalStorage();
        // 註冊客戶端webview的關閉生命鉤子事件
        this.beforeLeaveWebview();
    
        this._observer = new IntersectionObserver(function (entries, observer) {
          entries.forEach(entry => {
            // 這段邏輯,是每一個商品進入視窗時都會觸發的
            if (entry.isIntersecting) {
              // 清楚當前定時器
              clearTimeout(self._timer);
              // 我這裡是直接把商品相關的資料直接放DOM上面了  比如 <div {...什麼id  class style等屬性} :data-dot="渲染商品流時自行加上自身屬性" ></div>
              const ctm = entry.target.attributes['data-dot'].value;
              // 把收集到的資料新增進待上報的資料陣列中
              self.dotDataArr.push(ctm);
              // 收集到該商品的資料後,取消對該商品DOM的觀察
              self._observer.unobserve(entry.target);
              // 超過一定數量打點,打完點會刪除這一批
              if (self.dotDataArr.length >= self.maxNum) {
                self.dot();
              } else {
                self.storeIntoLocalstorage(self.dotDataArr);
                if (self.dotDataArr.length > 0) {
                  //,只要有新的ctm進來  接下來如果沒增加  自動2秒後打
                  self._timer = window.setTimeout(function () {
                    self.dot();
                  }, 2000)
                }
              }
            }
          })
        }, {
            root: null,
            rootMargin: "0px",
            threshold: 0.5 // 不一定非得全部露出來  這個閾值可以小一點點
          });
    
      }
    
      // 每個商品都會會通過全域性唯一的Exposure的例項來執行該add方法,將自己新增進觀察者中
      add(entry) {
        this._observer && this._observer.observe(entry.el)
      }
    
      dot() {
        // 同時刪除這批打點的ctms
        const dotDataArr = this.dotDataArr.splice(0, this.maxNum);
        DotData(dotDataArr);
        // 打完點,也順便更新一下localStorage
        this.storeIntoLocalstorage(this.dotDataArr);
      }
    
      storeIntoLocalstorage(dotDataArr) {
        // 。。。 存進localStorage中,具體什麼格式的字串自行定義就好
      }
    
      dotFromLocalStorage() {
        const ctmsStr = window.localStorage.getItem('dotDataArr');
        if (ctmsStr) {
          // 。。。如果有資料,就上報打點
        }
      }
    
      beforeLeaveWebview() {
        let win: any = window;
        // 自行跟客戶端童鞋約定該鉤子的實現就好
        injectEvent("webviewWillDisappear", () => {
          if (this.dotDataArr.length > 0) {
            DotData(this.dotDataArr);
          }
        })
      }
    }複製程式碼

    2、vue例項化+封裝指令

    [自定義指令 — Vue.js]

    // 入口JS檔案 main.js
    // 引入Exposure類
    // exp就是那個全域性唯一的例項
    const exp = new Exposure();
    
    // vue封裝一個指令,每個使用了該指令的商品都會自動add自身進觀察者中
    Vue.directive('exp-dot', {
        bind(el, binding, vnode) {
            exp.add({el: el, val: binding.value})
        }
    })複製程式碼

    3、商品使用

    迴圈時對每個商品使用指令即可

     :data-dot="item.dotData"就是我們要收集的資料

    <div 
        v-exp-dot 
        v-for="item in list" 
        :key="item.id" 
        class="" 
        :data-dot="item.dotData"
    >
      // ... 
    </div>複製程式碼

    在一開始的方案裡,我們打算每次手動觸發對一批商品的觀察,比如上拉載入生成了一批新的商品、手動互動載入了一批新的商品等,直接操作observer例項去觀察這一批新的商品 

    後面發現,跟業務邏輯耦合太重,而且一個頁面的全部商品不一定是乖乖的按順序由一個陣列迴圈渲染而成,還可能存在各種各樣的資源位,有各種各樣的出現理由,還不如直接讓每個商品自行觸發被觀察


    五、更多

    感謝您耐心看到這裡,希望有所收穫!

    我在學習過程中喜歡做記錄,分享的是自己在前端之路上的一些積累和思考,希望能跟大家一起交流與進步,更多文章請看【amandakelake的Github部落格】


    參考

    [基於IntersectionObserver的曝光統計測試 | xgfe]

    [Beforeunload打點丟失原因分析及解決方案]



    相關文章