如何实现元素的曝光监测

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:霁明

一些名词解释

曝光

页面上某一个元素、组件或模块被用户浏览了,则称这个元素、组件或模块被曝光了。
视图元素

将页面上展示的元素、组件或模块统称为视图元素。
可见比例

视图元素在可视区域面积/视图元素整体面积。
有效停留时长

视图元素由不可见到可见,满足可见比例并且保持可见状态的持续的一段时间。
重复曝光

在同一页面,某个视图元素不发生DOM卸载或页面切换的情况下,发生的多次曝光称为重复曝光。例如页面上某个视图元素,在页面来回滚动时,则会重复曝光。

如何监测曝光

需要考虑的一些问题

曝光条件

页面上某一视图元素的可见比例达到一定值(例如0.5),且有效停留时间达到一定时长(例如500ms),则称该视图元素被曝光了。
如何检测可见比例

使用 IntersectionObserver api 对元素进行监听,通过 threshold 配置项设置可见比例,当达到可见比例时,观察器的回调就会执行。

IntersectionObserver 使用示例:

javascript 复制代码
let callback = (entries, observer) => {
  entries.forEach((entry) => {
    // 每个条目描述一个目标元素观测点的交叉变化:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};
let options = {
  threshold: 1.0,
};
let observer = new IntersectionObserver(callback, options);

let target = document.querySelector("#listItem");
observer.observe(target);

如何监听动态元素

使用 IntersectionObserver 对元素进行监听之前,需要先获取到元素的 DOM,但对于一些动态渲染的元素,则无法进行监听。所以,需要先监听DOM元素是否发生挂载或卸载,然后对元素动态使用IntersectionObserver 进行监听,可以使用 MutationObserver 对 DOM变更进行监听。

MutationObserver的使用示例:

javascript 复制代码
// 选择需要观察变动的节点
const targetNode = document.getElementById("some-id");

// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };

// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
  for (let mutation of mutationsList) {
    if (mutation.type === "childList") {
      console.log("A child node has been added or removed.");
    } else if (mutation.type === "attributes") {
      console.log("The " + mutation.attributeName + " attribute was modified.");
    }
  }
};

// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);

// 以上述配置开始观察目标节点
observer.observe(targetNode, config);

// 之后,可停止观察
observer.disconnect();

如何监听停留时长

维护一个观察列表,元素可见比例满足要求时,将该元素信息(包含曝光开始时间)添加到列表,当元素退出可视区域时(可见比例小于设定值),用当前时间减去曝光开始时间,则可获得停留时长。

总体实现

实现一个exposure方法,支持传入需要检测曝光的元素信息(需包含className),使用 IntersectionObserver 和 MutationObserver 对元素进行动态监听。

  • 初始化时,根据className查找出已渲染的曝光监测元素,然后使用IntersectionObserver统一监听,如果有元素发生曝光,则触发对应曝光事件;
  • 对于一些动态渲染的曝光监测元素,需要使用MutationObserver监听dom变化。当有节点新增时,新增节点若包含曝光监测元素,则使用IntersectionObserver进行监听;当有节点被移除时,移除节点若包含曝光监测元素,则取消对其的监听;
  • 维护一个observe列表,元素开始曝光时将元素信息添加到列表,元素退出曝光时如果曝光时长符合规则,则触发对应曝光事件,并在observe列表中将该元素标记为已曝光,已曝光后再重复曝光则不进行采集。如果元素在DOM上被卸载,则将该元素在observe列表中的曝光事件删除,下次重新挂载时,则重新采集。
  • 设置一个定时器,定时检查observe列表,若列表中有未完成曝光且符合曝光时长规则的元素,则触发其曝光事件,并更新列表中曝光信息。

初始化流程

元素发生挂载或卸载过程

元素曝光过程

代码实现

typescript 复制代码
const exposure = (trackElems?: ITrackElem[]) => {
  const trackClassNames =
    trackElems
    ?.filter((elem) => elem.eventType === TrackEventType.EXPOSURE)
    .map((elem) => elem.className) || [];

  const intersectionObserver = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        const entryElem = entry.target;
        const observeList = getObserveList();
        let expId = entryElem.getAttribute(EXPOSURE_ID_ATTR);

        if (expId) {
          // 若已经曝光过,则不进行采集
          const currentItem = observeList.find((o) => o.id === expId);
          if (currentItem.hasExposed) return;
        }

        if (entry.isIntersecting) {
          if (!expId) {
            expId = getRandomStr(8);
            entryElem.setAttribute(EXPOSURE_ID_ATTR, expId);
          }
          const exit = observeList.find((o) => o.id === expId);
          if (!exit) {
            // 把当前曝光事件推入observe列表
            const trackElem = trackElems.find((item) =>
              entryElem?.classList?.contains(item.className)
                                             );
            const observeItem = { ...trackElem, id: expId, time: Date.now() };
            observeList.push(observeItem);
            setObserveList(observeList);
          }
        } else {
          if (!expId) return;
          const currentItem = observeList.find((o) => o.id === expId);
          if (currentItem) {
            if (Date.now() - currentItem.time > 500) {
              // 触发曝光事件,并更新observe列表中的曝光信息
              tracker.track(
                currentItem.event,
                TrackEventType.EXPOSURE,
                currentItem.params
              );
              currentItem.hasExposed = true;
              setObserveList(observeList);
            }
          }
        }
      });
    },
    { threshold: 0.5 }
  );

  const observeElems = (queryDom: Element | Document) => {
    trackClassNames.forEach((name) => {
      const elem = queryDom.getElementsByClassName?.(name)?.[0];
      if (elem) {
        intersectionObserver.observe(elem);
      }
    });
  };

  const mutationObserver = new MutationObserver((mutationList) => {
    mutationList.forEach((mutation) => {
      if (mutation.type !== 'childList') return;

      mutation.addedNodes.forEach((node: Element) => {
        observeElems(node);
      });

      mutation.removedNodes.forEach((node: Element) => {
        trackClassNames.forEach((item) => {
          const elem = node.getElementsByClassName?.(item)?.[0];
          if (!elem) return;
          const expId = elem.getAttribute('data-exposure-id');
          if (expId) {
            const observeList = getObserveList();
            const index = observeList.findIndex((o) => o.id === expId);
            if (index > -1) {
              // 元素被卸载时,将其曝光事件从列表删除
              observeList.splice(index, 1);
              setObserveList(observeList);
            }
          }
          intersectionObserver.unobserve(elem);
        });
      });
    });
  });

  observeElems(document);
  mutationObserver.observe(document.body, {
    subtree: true,
    childList: true,
  });

  const timer = setInterval(() => {
    // 检查observe队列,若队列中有符合曝光时长规则的元素,则修改曝光状态,并触发曝光事件。
    const observeList = getObserveList();
    let shouldUpdate = false;
    observeList.forEach((o) => {
      if (!o.hasExposed && Date.now() - o.time > 500) {
        tracker.track(o.event, TrackEventType.EXPOSURE, o.params);
        o.hasExposed = true;
        shouldUpdate = true;
      }
    });
    if (shouldUpdate) {
      setObserveList(observeList);
    }
  }, 3000);

  return () => {
    mutationObserver.disconnect();
    intersectionObserver.disconnect();
    clearInterval(timer);
    removeObserveList();
  };
};

export default exposure;

最后

欢迎关注【袋鼠云数栈UED团队】~

袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

相关推荐
刻刻帝的海角9 小时前
CSS 颜色
前端·css
python算法(魔法师版)1 天前
html,css,js的粒子效果
javascript·css·html
LBJ辉1 天前
1. 小众但非常实用的 CSS 属性
前端·css
学不完了是吧2 天前
html、js、css实现爱心效果
前端·css·css3
Zaly.2 天前
【前端】CSS实战之音乐播放器
前端·css
孤客网络科技工作室2 天前
不使用 JS 纯 CSS 获取屏幕宽高
开发语言·javascript·css
m0_748247552 天前
【HTML+CSS】使用HTML与后端技术连接数据库
css·数据库·html
肖老师xy2 天前
css动画水球图
前端·css
LBJ辉2 天前
2. CSS 中的单位
前端·css
wang.wenchao2 天前
十六进制文本码流转pcap(text2pcap)
前端·css