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

1、背景介绍

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

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

最直白的两种方法 前端

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

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



2、Intersection Observer API 

整个曝光打点方案基于[Intersection Observer API - Web API 接口参考 | MDN] node

这个API还比较新,至于兼容性问题可用[IntersectionObserver/polyfill· w3c]解决,本质上也是用getBoundingClientRect计算位置,具体怎么实现能够去看看它的源码
git


3、数据埋点思路

  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关闭的生命钩子事件(须要客户端童鞋支持),离开前所有打掉


4、代码实现

一、封装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);
      }
    })
  }
}复制代码

二、vue实例化+封装指令

[自定义指令 — Vue.js]
github

// 入口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})
    }
})复制代码

三、商品使用

循环时对每一个商品使用指令便可web

 :data-dot="item.dotData"就是咱们要收集的数据
算法

<div 
    v-exp-dot 
    v-for="item in list" 
    :key="item.id" 
    class="" 
    :data-dot="item.dotData"
>
  // ... 
</div>复制代码

在一开始的方案里,咱们打算每次手动触发对一批商品的观察,好比上拉加载生成了一批新的商品、手动交互加载了一批新的商品等,直接操做observer实例去观察这一批新的商品 

后面发现,跟业务逻辑耦合过重,并且一个页面的所有商品不必定是乖乖的按顺序由一个数组循环渲染而成,还可能存在各类各样的资源位,有各类各样的出现理由,还不如直接让每一个商品自行触发被观察


5、更多

感谢您耐心看到这里,但愿有所收获!

我在学习过程当中喜欢作记录,分享的是本身在前端之路上的一些积累和思考,但愿能跟你们一块儿交流与进步,更多文章请看【amandakelake的Github博客】


参考

[基于IntersectionObserver的曝光统计测试 | xgfe]

[Beforeunload打点丢失缘由分析及解决方案]

相关文章
相关标签/搜索