前端实现曝光埋点方案 - React HOC + Intersection Observer

7,744 阅读3分钟

埋点方案介绍

首先曝光埋点是指记录某个商品(dom元素)出现在指定视图下的频率,平台可通过大数据分析出用户的习惯,从而达到增加用户的购买力的效果。

dom滑动至可视区域,印入脑海的第一种方案是监听滚动事件,通过Element.getBoundingClientRect() 计算目标元素与视图的位置,然而现实是残酷的,且不说getBoundingClientRect Api会引起回流,造成性能问题(当然这个是主要问题),要计算目标元素的位置,对于我这种伸手党的前端开发来说也是很恶心的好吧。

苦苦搜寻其他方案,当我看到Intersection Observer API的时候,似乎看到了希望,话不多说直接上图,其中还说明Element.getBoundingClientRect() 的性能问题。

Intersection Observer

Intersection Observer API 用法

var options = {
   root: document.querySelector('#scrollArea'), // 根dom元素, null 默认为浏览器视图
   rootMargin: '0px', // root元素的外边距
   threshold: 1.0 // 目标元素与根元素相交程度触发cb - [0 - 1]
}
// 创建一个observer 实例
var observer = new IntersectionObserver(callback, options);
// 目标元素
var target = document.querySelector('#listItem');
observer.observe(target);

详情用法请参考 Intersection Observer API - Web API 接口参考 | MDN

兼容性解决方案请参考 IntersectionObserver polyfill

项目实践

实现思路

  1. 通过new IntersectionObserver() 初始化一个全局observer实例.
  2. 找到目标dom元素,通过observer.observe(element),将目标dom节点添加至观察列表中。
  3. 考虑一下延迟上报,针对列表无限下拉,导致并发问题。我们通过一个构建dataList数组,存储曝光的上报数据,并设置maxNum,如果曝光的数量达到maxNum 直接上报,如果没有达到开启一个定时器,定时上报。
  4. 每次上报之后,需要把数据从dataList 中移除,避免重复上报。

代码实现

  1. 封装一个Exposure 类,并暴露出一个 exposure实例
// polyfill 解决兼容性问题
import 'intersection-observer';
// 延迟时间,节流作用
IntersectionObserver.prototype['THROTTLE_TIMEOUT'] = 300;

class Exposure {
  constructor(maxNum = 10) {
    this.dataList = [];
    this.maxNum = maxNum;
    this.time = 0; // 延迟上报时间
    this.dataList = [];
    this.maxNum = maxNum; // 一次上报最大个数
    this._observer = null;
    this._timer = null;
    this.init();
  }

  // 初始化
  init() {
    const self = this;
    this._observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) { // 进入视图触发
          try {
            self._timer && clearTimeout(self._timer); // 清除定时器
            const eventParam = entry.target.attributes['data-param'].value;
            const eventId = entry.target.attributes['data-eventId'].value;
            self.dataList.push({ eventId, eventParam });
            // 已经上报的节点、取消对该DOM的观察
            self._observer.unobserve(entry.target);
            // 超出最大长度直接上报
            if (self.dataList.length >= self.maxNum) {
              self.send();
            } else if (self.dataList.length > 0) {
              self._timer = setTimeout(() => { // 定时上报
                self.send();
              }, self.time);
            }
          } catch (err) {
            console.log(err);
          }
        }
      })
    }, {
      root: document.querySelector('#app'),
      rootMargin: "0px",
      threshold: 1 // 目标dom出现在视图的比例 0 - 1
    });
  }

  // 添加至观察列表
  add(entry) {
    const { el } = entry || {};
    this._observer && this._observer.observe(el)
  }

  // 触发上报数据
  send() {
    console.log('---上报埋点数据---');
    const data = this.dataList.slice(0, this.maxNum);
    window.ZM_JSSDK && window.ZM_JSSDK.sendStuEvent(data)
  }

  // 组件销毁,数据全部上报
  beforeUnmount() {
    console.log('---离开页面,上报埋点数据---');
    const data = this.dataList;
    window.ZM_JSSDK && window.ZM_JSSDK.sendStuEvent(data)
  }
}

export default new Exposure();
  1. 封装 React HOC
import React, { useEffect } from "react";
import exposure from "../../utils/exposure";

// 这里使用hoc,目的在于属性封装
const exposureHoc = (WrappedComponent) => (props) => {
  const { index, eventId, sendData = true, eventParam } = props;
  // did mount 添加至观察列表
  useEffect(() => {
    sendData && exposure.add({ el: document.getElementById(`paper-content-${index}`) })
  }, []);
  const value = { index: index, ...eventParam };
  // 通过dom data 属性设置埋点参数
  return (
    <div id={`paper-content-${index}`} data-eventId={eventId} data-param={JSON.stringify(value)}>
      <WrappedComponent {...props} />
    </div>
  )
}

export default exposureHoc;
  1. 使用
// BookItem 组件
import React from 'react';
import exposureHoc from 'components/Exposure';

const BookItem = (props) => {
  const { index, subItem } = props;
  return (
    <div key={subItem.bookId}>
      <img src={subItem.cover} width='136px' height='178px' />
      <span>{subItem.bookName}</span>
    </div>
  )
}

export default exposureHoc(BookItem);

// 父组件
<BookItem
  index={index}
  subItem={subItem}
  eventId='07_learnmaterial_materiallist_show' // 埋点事件ID
  eventParam={{ ... }} // 埋点参数
/>

总结

以上就是我本次针对前端曝光埋点的实现方案,可以根据业务继续封装,如有问题请多多交流。