「前端曝光埋点上报」实现方案

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

现状

为准确分析各前端页面实际对用户的吸引力,需要统计的页面元素的曝光数据。曝光的含义比较模糊,具体的统计方式也比较麻烦,本文分享一个前端曝光埋点上报的实现方案。

方案

为了统计曝光数据,首先要做的是,定义什么是曝光,然后制定上报数据的策略。

根据我们业务的实际情况,我们可以这样设计:

  1. 曝光定义:dom元素出现在屏幕窗口中,被用户看到,且停留时间超过500ms,才算一次曝光。dom元素退出窗口后重新进入窗口,再停留500ms,记为第二次曝光。
  2. 数据上报:需要尽量减少上报次数(1)定时器每N秒检查一次,如果有待上报数据就请求接口上报(2)如果待上报数据大于M条,直接上报,不需要等待N秒。

开始操作

整体实现

具体的代码实现如下:

  1. 使用IntersectionObserver观察是否出现和消失在窗口,用IntersectionObserver polyfill提升兼容性。
  2. 用vue的指令,实现上报数据的绑定,最后使用的时候,只需要为需要上报的元素,加上v-treport=“上报的数据”。
  3. 在指令绑定的时候,为dom元素绑定report-data和guid属性,具体值分别为待上报数据和唯一ID。
  4. 具体观测和上报曝光的逻辑,后面具体讲。

使用方式

7dffeea35e28f5248603f5d78102b_w1016_h210.png

绑定指令后的元素:

acc8187bf025ee4ab27958381ea92_w2588_h338.png

具体细节

元素X进入窗口

  1. 元素X进入窗口,记录到sessionStorage的to-observe队列(如果已存在,就不加入队列)(使用sessionStorage,是因为,浏览器关闭了不在需要计算观测结果)
  2. 结构为 {stime:观测到的时间, id:guid, data:待上报数据,hasObserve:false}

元素X退出窗口

  1. 从to-observe队列获取X的stime,如果(当前时间-stime)>=500ms而且hasObserve为false,将X元素的数据推入localStorage的to-report的队列(使用localStorage,浏览器关闭了,在下次进来,还可以把to-report未上报的进行上报)。
  2. 无论何种情况,元素X都要推出to-observe队列。

曝光定时器(每500ms检查一次)

  1. 如果to-observe队列中存在(当前时间-stime)>=500ms的X,hasObserve置为true,将X元素的数据推入to-report的队列

上报定时器(每1000ms检查一次)

  1. 如果to-report队列存在记录,上报并从to-report移出。
  2. 如果推入to-report队列时,队列长度大于M,直接上报。

fcb10adb7848a052d388769e95b4e_w1152_h698.png

观测元素的几种情况:

  • A:进入窗口,500ms后退出窗口,需要上报
  • B:进入窗口,没有退出窗口,超过了500ms,需要上报
  • C:进入窗口,不到500ms退出窗口,不需要上报

代码实现

require('intersection-observer');

const MIN_OBSERVE_TIME = 500;
const OBSERVE_REPEAT_TIME = 1000;
const REPORT_REPEAT_TIME = 1000;

// 获取IntersectionObserver的单例
class ReportObserver {
  constructor() {
    this.instance = null;
    this.intersectionObserver = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          handleEnter(entry);
        } else {
          handleExit(entry);
        }
      });
    });
    // 初始化定时器
    initInterval();
  }
  observe(el, reportData) {
    el.setAttribute('report-data', JSON.stringify(reportData));
    el.setAttribute('guid', guid());
    this.intersectionObserver.observe(el);
  }
  // 获取IntersectionObserver的实例
  static getInstance() {
    if (!this.instance) {
      this.instance = new ReportObserver();
    }
    return this.instance;
  }
}

// 元素X进入窗口,记录到sessionStorage的to-observe队列(如果已存在,就不加入队列),
// 结构为 {stime:观测到的时间, id:元素ID, data:待上报数据,hasObserve:false}。
const handleEnter = function (entry) {
  const dom =  entry.target;
  const data = dom.getAttribute('report-data');
  const id = dom.getAttribute('guid');
  const stime = new Date().getTime();
  const hasObserve = false;

  const observeData = { id, data, stime, hasObserve };
  if (!findToObserve(id)) {
    pushToObserve(observeData);
  }
};

// 元素X退出窗口:
// 1、从to-observe队列获取X的stime,如果(当前时间-stime)>=500ms而且hasObserve为false,将X元素的数据推入to-report的队列。
// 2、无论何种情况,元素X都要推出to-observe队列。
const handleExit = function (entry) {
  const dom =  entry.target;
  const id = dom.getAttribute('guid');
  const etime = new Date().getTime();

  const value = findToObserve(id);
  if (value && etime - value.stime >= MIN_OBSERVE_TIME && !value.hasObserve) {
    pushToReport(value);
  }
  deleteFromToObserve(id);
};

// 初始化定时器
const initInterval = function () {
  // 曝光定时器
  setInterval(() => {
    // 如果to-observe队列中存在(当前时间-stime)>=500ms并且hasObserve为false的X,将X的hasObserve置为true,并推入to-report的队列
    toObserveList().forEach((value) => {
      const etime = new Date().getTime();
      if (etime - value.stime >= MIN_OBSERVE_TIME && !value.hasObserve) {
        value.hasObserve = true;
        pushToObserve(value);
        pushToReport(value);
      }
    });
  }, OBSERVE_REPEAT_TIME);
  // 上报定时器
  setInterval(() => {
    // 如果to-report队列存在记录,上报并从to-report移出。
    if (toReportList.length) {
      postReportData(toReportList);
      clearToReport();
    }
  }, REPORT_REPEAT_TIME);
};

const postReportData = function (dataList) {
  // 调用后台接口上报数据
};

export default {
  bind(el, binding) {
    if (
      'IntersectionObserver' in window
        && 'IntersectionObserverEntry' in window
        && 'intersectionRatio' in window.IntersectionObserverEntry.prototype
    ) {
      // 开始监听
      ReportObserver.getInstance().observe(el, binding.value);;
    }
  },

};
复制代码

参考文献

Intersection_Observer_API

往期好文

“告别烂代码”

2022代码规范最佳实践(附web和小程序最优配置示例)

【前端探索】告别烂代码!用责任链模式封装网络请求

【前端探索】告别烂代码第二期!用策略模式封装分享组件

代码人生

【三年前端开发的思考】如何有效地阅读需求?

前端踩坑必看指南

【前端探索】图片加载优化的最佳实践

【前端探索】移动端H5生成截图海报的探索

【前端探索】H5获取用户定位?看这一篇就够了

【前端探索】微信小程序跳转的探索——开放标签为什么存在?

【前端探索】vConsole花式用法

猜你喜欢

转载自juejin.im/post/7083122975502761998