前端曝光数据埋点——Intersection Observer+vue指令

9,124 阅读5分钟

一、背景介绍

在电商产品中(可以打开你的淘宝、天猫、京东App),通过对商品的曝光进行数据埋点,就能反推出用户的行为和交互习惯,从而优化推荐和搜索算法以及交互,最终的目的当然是为了增加用户购买力。 

曝光:商品出现在用户眼前,也就是浏览器视窗,可谓之曝光

最直白的两种方法 

这两种办法都能用,但是getBoundingClientRect这个API是会引起页面回流的,使用不当容易导致性能问题。 

基于此,浏览器特意为我们打造了一个[Intersection Observer API - Web API | MDN],把性能相关的细节都处理掉,让开发者只关心业务逻辑即可



二、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,不等定时器,直接全部上报
  4. 打点不难,难的是不漏以及不重复上报数据,用户离开页面前的边界数据处理
    • 浏览器环境: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打点丢失原因分析及解决方案]