找回密码
 立即注册
首页 业界区 业界 如何实现元素的曝光监测

如何实现元素的曝光监测

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

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

需要考虑的一些问题

曝光条件
页面上某一视图元素的可见比例达到一定值(例如0.5),且有效停留时间达到一定时长(例如500ms),则称该视图元素被曝光了。
如何检测可见比例
使用 IntersectionObserver api 对元素进行监听,通过 threshold 配置项设置可见比例,当达到可见比例时,观察器的回调就会执行。
IntersectionObserver 使用示例:
  1. let callback = (entries, observer) => {
  2.   entries.forEach((entry) => {
  3.     // 每个条目描述一个目标元素观测点的交叉变化:
  4.     //   entry.boundingClientRect
  5.     //   entry.intersectionRatio
  6.     //   entry.intersectionRect
  7.     //   entry.isIntersecting
  8.     //   entry.rootBounds
  9.     //   entry.target
  10.     //   entry.time
  11.   });
  12. };
  13. let options = {
  14.   threshold: 1.0,
  15. };
  16. let observer = new IntersectionObserver(callback, options);
  17. let target = document.querySelector("#listItem");
  18. observer.observe(target);
复制代码
如何监听动态元素
使用 IntersectionObserver 对元素进行监听之前,需要先获取到元素的 DOM,但对于一些动态渲染的元素,则无法进行监听。所以,需要先监听DOM元素是否发生挂载或卸载,然后对元素动态使用IntersectionObserver 进行监听,可以使用 MutationObserver 对 DOM变更进行监听。
MutationObserver的使用示例:
  1. // 选择需要观察变动的节点
  2. const targetNode = document.getElementById("some-id");
  3. // 观察器的配置(需要观察什么变动)
  4. const config = { attributes: true, childList: true, subtree: true };
  5. // 当观察到变动时执行的回调函数
  6. const callback = function (mutationsList, observer) {
  7.   for (let mutation of mutationsList) {
  8.     if (mutation.type === "childList") {
  9.       console.log("A child node has been added or removed.");
  10.     } else if (mutation.type === "attributes") {
  11.       console.log("The " + mutation.attributeName + " attribute was modified.");
  12.     }
  13.   }
  14. };
  15. // 创建一个观察器实例并传入回调函数
  16. const observer = new MutationObserver(callback);
  17. // 以上述配置开始观察目标节点
  18. observer.observe(targetNode, config);
  19. // 之后,可停止观察
  20. observer.disconnect();
复制代码
如何监听停留时长
维护一个观察列表,元素可见比例满足要求时,将该元素信息(包含曝光开始时间)添加到列表,当元素退出可视区域时(可见比例小于设定值),用当前时间减去曝光开始时间,则可获得停留时长。
总体实现

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

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

1.png

元素发生挂载或卸载过程

2.png

元素曝光过程

3.png

代码实现
  1. const exposure = (trackElems?: ITrackElem[]) => {
  2.   const trackClassNames =
  3.     trackElems
  4.     ?.filter((elem) => elem.eventType === TrackEventType.EXPOSURE)
  5.     .map((elem) => elem.className) || [];
  6.   const intersectionObserver = new IntersectionObserver(
  7.     (entries) => {
  8.       entries.forEach((entry) => {
  9.         const entryElem = entry.target;
  10.         const observeList = getObserveList();
  11.         let expId = entryElem.getAttribute(EXPOSURE_ID_ATTR);
  12.         if (expId) {
  13.           // 若已经曝光过,则不进行采集
  14.           const currentItem = observeList.find((o) => o.id === expId);
  15.           if (currentItem.hasExposed) return;
  16.         }
  17.         if (entry.isIntersecting) {
  18.           if (!expId) {
  19.             expId = getRandomStr(8);
  20.             entryElem.setAttribute(EXPOSURE_ID_ATTR, expId);
  21.           }
  22.           const exit = observeList.find((o) => o.id === expId);
  23.           if (!exit) {
  24.             // 把当前曝光事件推入observe列表
  25.             const trackElem = trackElems.find((item) =>
  26.               entryElem?.classList?.contains(item.className)
  27.                                              );
  28.             const observeItem = { ...trackElem, id: expId, time: Date.now() };
  29.             observeList.push(observeItem);
  30.             setObserveList(observeList);
  31.           }
  32.         } else {
  33.           if (!expId) return;
  34.           const currentItem = observeList.find((o) => o.id === expId);
  35.           if (currentItem) {
  36.             if (Date.now() - currentItem.time > 500) {
  37.               // 触发曝光事件,并更新observe列表中的曝光信息
  38.               tracker.track(
  39.                 currentItem.event,
  40.                 TrackEventType.EXPOSURE,
  41.                 currentItem.params
  42.               );
  43.               currentItem.hasExposed = true;
  44.               setObserveList(observeList);
  45.             }
  46.           }
  47.         }
  48.       });
  49.     },
  50.     { threshold: 0.5 }
  51.   );
  52.   const observeElems = (queryDom: Element | Document) => {
  53.     trackClassNames.forEach((name) => {
  54.       const elem = queryDom.getElementsByClassName?.(name)?.[0];
  55.       if (elem) {
  56.         intersectionObserver.observe(elem);
  57.       }
  58.     });
  59.   };
  60.   const mutationObserver = new MutationObserver((mutationList) => {
  61.     mutationList.forEach((mutation) => {
  62.       if (mutation.type !== 'childList') return;
  63.       mutation.addedNodes.forEach((node: Element) => {
  64.         observeElems(node);
  65.       });
  66.       mutation.removedNodes.forEach((node: Element) => {
  67.         trackClassNames.forEach((item) => {
  68.           const elem = node.getElementsByClassName?.(item)?.[0];
  69.           if (!elem) return;
  70.           const expId = elem.getAttribute('data-exposure-id');
  71.           if (expId) {
  72.             const observeList = getObserveList();
  73.             const index = observeList.findIndex((o) => o.id === expId);
  74.             if (index > -1) {
  75.               // 元素被卸载时,将其曝光事件从列表删除
  76.               observeList.splice(index, 1);
  77.               setObserveList(observeList);
  78.             }
  79.           }
  80.           intersectionObserver.unobserve(elem);
  81.         });
  82.       });
  83.     });
  84.   });
  85.   observeElems(document);
  86.   mutationObserver.observe(document.body, {
  87.     subtree: true,
  88.     childList: true,
  89.   });
  90.   const timer = setInterval(() => {
  91.     // 检查observe队列,若队列中有符合曝光时长规则的元素,则修改曝光状态,并触发曝光事件。
  92.     const observeList = getObserveList();
  93.     let shouldUpdate = false;
  94.     observeList.forEach((o) => {
  95.       if (!o.hasExposed && Date.now() - o.time > 500) {
  96.         tracker.track(o.event, TrackEventType.EXPOSURE, o.params);
  97.         o.hasExposed = true;
  98.         shouldUpdate = true;
  99.       }
  100.     });
  101.     if (shouldUpdate) {
  102.       setObserveList(observeList);
  103.     }
  104.   }, 3000);
  105.   return () => {
  106.     mutationObserver.disconnect();
  107.     intersectionObserver.disconnect();
  108.     clearInterval(timer);
  109.     removeObserveList();
  110.   };
  111. };
  112. export default exposure;
复制代码
最后

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

  • 大数据分布式任务调度系统——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据领域的 SQL Parser 项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
  • 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
  • 一个针对 antd 的组件测试工具库——ant-design-testing

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册