Intersection Observer API 实现数据埋点 批量曝光

1,945 阅读3分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

基于NuxtJS 2.x构建的项目实现

官网介绍:MDN Web Docs 之 Intersection Observer API

  • 1、new IntersectionObserver() 实例化一个全局_observer,每个 DOM 节点自行把自己加入_observer 的观察列表 (此处会用 Vue 的指令来实现)
  • 2、当某个 DOM 节点进入视窗,收集该 DOM 的信息,存进一个全局数组 dotArr 中,然后取消对该 DOM 的观察
  • 3、从 dotArr 中取数据上传
    • 跑定时器,每隔 N 秒检查一次,如果 dotArr 有数据,就直接上报;
    • 如果 N 秒内,dotArr 的数据量大于某个量 maxNum,不等定时器,直接全部上报
  • 4、不漏以及不重复上报数据,用户离开页面前的边界数据处理
    • 浏览器环境:dotArr 同时存一份在 localStorage 中,同步更新数据(增加或者上报完后清空),如果用户真的在 N 秒的间隔内,而数据又不够最大上报量 maxNum 就离开了页面,那么这批数据就等用户下次再进页面时,直接从 localStorage 中取出来上传。当然如果这个用户再也不进页面或者清空了浏览器缓存,这一点点数据丢失是可以接受。

曝光监听

  1. 创建 intersection-observer.js 文件
// 安装 intersection-observer 插件
npm install intersection-observer --save-dev

// 创建观察文件 intersection-observer.js
import "intersection-observer";
import axios from "axios";

// 数据上报方法,即网络请求
const platformExposure = (dotDataArr) => {
  axios
    .post("/api/xxx/exposure", { para: { list: dotDataArr } })
};

// 节流的时间,默认是100ms
IntersectionObserver.prototype.THROTTLE_TIMEOUT = 300;

const localStorage = window.localStorage;

export default class Exposure {
  constructor(maxNum = 200) {
    this.dotDataArr = []; // 进入视窗的DOM节点的数据
    this.maxNum = maxNum;
    this.timeout = 1 * 1000 * 60; // 间隔时间上传一次
    this._timer = 0;
    this._observer = null; // 观察者的集合
    this.init(); // 全局只会实例化一次Exposure类
  }

  init() {
    const self = this;
    // init只会执行一次,边界处理方法,把浏览器localStorage里面的剩余数据上传
    this.dotFromLocalStorage();

    this._observer = new IntersectionObserver(
      (entries, observer) => {
        // 每一个产品进入视窗时都会触发
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            // 清除当前定时器
            clearTimeout(self._timer);
            // 把相关的数据直接放DOM上面了,比如 <div :data-dot="哈哈" ></div>
            // const ctm = entry.target.attributes['data-dot'].value
            const dataset = entry.target.dataset;
            const ctm = {
              platform_id: dataset.id, // 产品id 必填
            };
            // 收集数据,进待上报的数据数组
            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进来,接下来如果没增加,自动n秒后打点
                self._timer = window.setTimeout(() => {
                  self.dot();
                }, self.timeout);
              }
            }
          }
        });
      },
      {
        root: null, // 指定根目录,也就是当目标元素显示在这个元素中时会触发监控回调。 默认值为null,即浏览器窗口
        rootMargin: "0px", // 设定root元素的边框区域
        threshold: 0.5, // number或number数组,控制target元素进入root元素中可见性超过的阙值,达到阈值会触发函数,也可以使用数据来让元素在进入时在不同的可见度返回多次值
      }
    );
  }

  // 每个DOM元素通过全局唯一的Exposure的实例来执行该add方法,将自己添加进观察者中
  add(entry) {
    this._observer && this._observer.observe(entry.el);
  }

  // 上传并更新缓存
  dot() {
    const dotDataArr = this.dotDataArr.splice(0, this.maxNum);
    platformExposure(dotDataArr);
    this.storeIntoLocalstorage(this.dotDataArr);
  }

  // 缓存数据
  storeIntoLocalstorage(dotDataArr) {
    localStorage.setItem("dotDataArr", JSON.stringify(dotDataArr));
  }

  // 上传数据
  dotFromLocalStorage() {
    const ctmsStr = JSON.parse(localStorage.getItem("dotDataArr"));
    if (ctmsStr && ctmsStr.length > 0) {
      platformExposure(ctmsStr);
    }
  }
}

曝光指令

  1. 完成曝光指令文件 directives.client.js
import Vue from "vue";
import Exposure from "./intersection-observer";
// exp 全局唯一的实例
const exp = new Exposure();

Vue.directive("exp-dot", {
  bind(el, binding, vnode) {
    // 每个使用了该指令的商品都会自动add自身进观察者中
    exp.add({ el, val: binding.value });
  },
  update(newValue, oldValue) {
    // 值更新时的工作
    // 也会以初始值为参数调用一次, 此时可以根据传值类型来进行相应埋点行为的请求处理
  },
  unbind() {
    // 清理工作
  },
});
  1. 在配置文件nuxt.config.js中引入指令文件directives.client.js,其中client代表只在客户端生效
// nuxt.config.js
module.exports = {
  mode: "universal",
  plugins: [{ src: "~plugins/directives.client.js" }],
};

实战使用

核心使用就是v-exp-dot

<template>
  <div class="mescroll">
    <div class="list-product">
      <div
        class="list-item"
        v-for="(item, index) in listData"
        :key="item.id"
        :item="item"
        :data-id="item.id"
        :data-url="item.url"
        v-exp-dot
      />
    </div>
  </div>
</template>