如何实现元素的曝光监测

· 浏览次数 : 40

小编点评

本文主要介绍了袋鼠云数栈 UED 团队在数据中台产品开发过程中,如何利用前端技术实现元素曝光、动态元素监听和停留时长统计等功能。文章首先解释了一些相关概念,然后通过具体的实现示例,展示了如何使用 IntersectionObserver、MutationObserver 和其他前端技术来实现这些功能。 1. **名词解释**: - 视图元素:页面上展示的元素、组件或模块。 - 可见比例:视图元素在可视区域面积与视图元素整体面积的比例。 - 有效停留时长:视图元素由不可见到可见,并满足可见比例且保持可见状态的持续时间。 - 重复曝光:在同一个页面上,某个视图元素在不发生DOM卸载或页面切换的情况下,发生的多次曝光。 2. **曝光监测**: - 使用 IntersectionObserver API 监听元素可见比例,当可见比例达到一定值且有效停留时间达到一定时长时,认为该元素被曝光。 - 示例代码展示了如何使用 IntersectionObserver API 进行元素曝光检测。 3. **动态元素监听**: - 对于动态渲染的元素,需要先监听 DOM 元素的挂载或卸载事件,然后对元素使用 IntersectionObserver 进行监听。 - 示例代码展示了如何使用 MutationObserver 监听 DOM 变化,并在元素发生变化时触发相应的处理逻辑。 4. **停留时长统计**: - 维护一个观察列表,记录元素的曝光开始时间和退出可视区域的时间。 - 当元素退出可视区域且曝光时长符合规则时,触发曝光事件,并更新观察列表中的曝光信息。 5. **实施步骤**: - 初始化时,根据元素类名查找已渲染的曝光监测元素,并使用 IntersectionObserver 统一监听。 - 动态渲染的元素需要使用 MutationObserver 进行监听。 - 定时检查观察列表,若列表中有未完成曝光且符合曝光时长规则的元素,则触发其曝光事件,并更新列表中曝光信息。 总的来说,袋鼠云数栈 UED 团队通过结合前端技术的力量,为开发者提供了一套完整的数据中台产品开发解决方案,包括元素曝光监测、动态元素监听和停留时长统计等功能,以支持前端道路上的各种需求。

正文

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

本文作者:霁明

一些名词解释

曝光
页面上某一个元素、组件或模块被用户浏览了,则称这个元素、组件或模块被曝光了。
视图元素
将页面上展示的元素、组件或模块统称为视图元素。
可见比例
视图元素在可视区域面积/视图元素整体面积。
有效停留时长
视图元素由不可见到可见,满足可见比例并且保持可见状态的持续的一段时间。
重复曝光
在同一页面,某个视图元素不发生DOM卸载或页面切换的情况下,发生的多次曝光称为重复曝光。例如页面上某个视图元素,在页面来回滚动时,则会重复曝光。

如何监测曝光

需要考虑的一些问题

曝光条件
页面上某一视图元素的可见比例达到一定值(例如0.5),且有效停留时间达到一定时长(例如500ms),则称该视图元素被曝光了。
如何检测可见比例
使用 IntersectionObserver api 对元素进行监听,通过 threshold 配置项设置可见比例,当达到可见比例时,观察器的回调就会执行。
IntersectionObserver 使用示例:

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的使用示例:

// 选择需要观察变动的节点
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列表,若列表中有未完成曝光且符合曝光时长规则的元素,则触发其曝光事件,并更新列表中曝光信息。

初始化流程

file

元素发生挂载或卸载过程

file

元素曝光过程

file

代码实现

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

与如何实现元素的曝光监测相似的内容:

如何实现元素的曝光监测

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。 本文作者:霁明 一些名词解释 曝光 页面上某一个元素、组件或模块被用户浏览了,则称这个元素、组件或模块被曝光了。 视图元素 将页面上展示的元素、组件或模块统称为视图元素

一文帮你搞定H5、小程序、Taro长列表曝光埋点

对于各种类型的埋点来说,曝光埋点往往最为复杂、需要用到的技术也最全面、如果实现方式不合理可能造成的影响也最大,因此本文将重点介绍曝光埋点尤其是长列表(或滚动视图)内元素曝光埋点的实现思路及避坑技巧

WPF/C#:如何实现拖拉元素

前言 在Canvas中放置了一些元素,需要能够拖拉这些元素,在WPF Samples中的DragDropObjects项目中告诉了我们如何实现这种效果。 效果如下所示: 拖拉过程中的效果如下所示: 具体实现 xaml页面 我们先来看看xaml:

渐变边框文字效果?CSS 轻松拿捏!

今天,有个群友问了我这么一个问题,如果不想切图,是否有办法实现带渐变边框的字体效果?如下所示: 本文,就将尝试一下,在 CSS 中,我们可以如何尽可能的实现这种渐变边框字体效果。 元素叠加 首先,比较容易想到的写法是通过元素叠加实现。 元素本身实现文字效果本身 通过元素的伪元素,配合 backgro

如何从0开始搭建 Vue 组件库

组件设计是通过对功能及视觉表达中元素的拆解、归纳、重组,并基于可被复用的目的,形成规范化的组件,通过多维度组合来构建整个设计方案,將这些组件整理在一起,便形成组件库。本文我们主要讲述基于Vant CLI的自建组件库。Vant CLI 是一个基于 Vite 实现的 Vue 组件库构建工具,通过 Vant CLI 可以快速搭建一套功能完备的 Vue 组件库。

文心一言 VS 讯飞星火 VS chatgpt (104)-- 算法导论10.1 2题

二、用go语言,说明如何在一个数组 A[1..n]中实现两个栈,使得当两个栈的元素个数之和不为 n 时,两者都不会发生上溢。要求 PUSH 和 POP 操作的运行时间为 O(1)。 文心一言: 在这个问题中,我们要在一个数组 A[1..n] 中实现两个栈,使得当两个栈的元素个数之和不为 n 时,两者

SICP:赋值和局部状态(Python实现)

前面我们介绍了组成程序的各种基本元素,看到了如何把基本过程和基本数据组合起来,构造出复合的实体。不过对于设计程序而言,这些手段还不够,我们还需要一些能够帮助我们构造起模块化(modular)的大型系统的策略。所谓模块化,也即使这些系统能够“自然地”划分为一些内聚(coherent)的部分,使这些部分可以分别进行开发和维护。接下来我们要研究两种特色很鲜明的组织策略,它们源自于对于系统结构的两种非常不

C#插入排序算法

插入排序实现原理 插入排序算法是一种简单、直观的排序算法,其原理是将一个待排序的元素逐个地插入到已经排好序的部分中。 具体实现步骤如下 首先咱们假设数组长度为n,从第二个元素开始,将当前元素存储在临时变量temp中。 从当前元素的前一个位置开始向前遍历,比较temp与每个已排序元素的值大小。 如果已

C#冒泡排序算法

冒泡排序实现原理 冒泡排序是一种简单的排序算法,其原理如下: 从待排序的数组的第一个元素开始,依次比较相邻的两个元素。 如果前面的元素大于后面的元素(升序排序),则交换这两个元素的位置,使较大的元素“冒泡”到右侧。 继续比较下一对相邻元素,重复步骤2,直到遍历到数组的倒数第二个元素。此时,最大的元素

Dotnet算法与数据结构:Hashset, List对比

哈希集A 是存储唯一元素的集合。它通过在内部使用哈希表来实现这一点,该哈希表为基本操作(如添加、删除和包含)提供恒定时间平均复杂度 (O(1))。此外,不允许重复元素,使其成为唯一性至关重要的场景的理想选择。另一方面,表示按顺序存储元素的动态数组。它允许重复元素并提供对元素的索引访问,使其适用于需要