如何實現元素的曝光監測

来源:https://www.cnblogs.com/dtux/p/18302635
-Advertisement-
Play Games

我們是袋鼠雲數棧 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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 當在UITableViewCell中載入網路圖片時,如果在圖片下載完成之前用戶滑動了UITableView,使得對應的UITableViewCell已經滑出屏幕,那麼這個被滑走的UITableViewCell是否還會顯示圖片,取決於如何處理圖片的載入和UITableViewCell的重用。 UITa ...
  • UITableView的重用機制避免了頻繁創建和銷毀單元格的開銷,使得在顯示大量數據時,保持流暢的用戶體驗和較低的資源消耗。。 當UITableView滾動時,屏幕上移出視圖的單元格會被回收到一個重用池中。當需要顯示新的單元格時,UITableView會首先檢查重用池中是否有可用的單元格。如果有,就 ...
  • 1. Vue3簡介 2020年9月18日,Vue.js發佈版3.0版本,代號:One Piece(n 經歷了:4800+次提交、40+個RFC、600+次PR、300+貢獻者 官方發版地址:Release v3.0.0 One Piece · vuejs/core 截止2023年10月,最新的公開版 ...
  • ‍ 寫在開頭 點贊 + 收藏 學會 前言 2020 年初突如其來的新冠肺炎疫情讓線下就醫渠道幾乎被切斷,在此背景下,微醫作為數字健康行業的領軍者通過線上問診等形式快速解決了大量急需就醫人們的燃眉之急。而作為微醫 Web 端線上問診中重要的一環-醫患之間的視頻問診正是應用了接下來講 ...
  • 在 Vue 3 中,組合式 API(Composition API)引入了新的響應式系統,使得狀態管理和邏輯復用變得更加靈活和強大。ref() 和 reactive() 是組合式 API 中兩個重要的響應式工具,它們各自有不同的使用場景和特性。在這篇博客中,我們將深入探討 ref() 和 react ...
  • Vue.js 中的 Ajax 處理:vue-resource 庫的深度解析 在現代前端開發中,Ajax 請求是與後端進行數據交互的關鍵技術。Vue.js 作為一個漸進式 JavaScript 框架,提供了多種方式來處理 Ajax 請求,其中 vue-resource 是一個較為常用的庫。儘管 vue ...
  • Vue.js 是一個漸進式的 JavaScript 框架,用於構建用戶界面。理解 Vue 的生命周期是掌握這個框架的關鍵之一。在這篇博客中,我們將深入探討 Vue 2 的生命周期,並通過代碼示例來展示每個生命周期鉤子的作用。 Vue 實例的生命周期 Vue 實例的生命周期可以分為四個主要階段: 創建 ...
  • 摘要:“探索Nuxt.js的useFetch:高效數據獲取與處理指南”詳述了Nuxt.js中useFetch函數的使用,包括基本用法、動態參數獲取、攔截器使用,及參數詳解。文章通過示例展示瞭如何從API獲取數據,處理動態參數,自定義請求和響應,以及useFetch和useAsyncData的參數選項... ...
一周排行
    -Advertisement-
    Play Games
  • 前言 微服務架構已經成為搭建高效、可擴展系統的關鍵技術之一,然而,現有許多微服務框架往往過於複雜,使得我們普通開發者難以快速上手並體驗到微服務帶了的便利。為瞭解決這一問題,於是作者精心打造了一款最接地氣的 .NET 微服務框架,幫助我們輕鬆構建和管理微服務應用。 本框架不僅支持 Consul 服務註 ...
  • 先看一下效果吧: 如果不會寫動畫或者懶得寫動畫,就直接交給Blend來做吧; 其實Blend操作起來很簡單,有點類似於在操作PS,我們只需要設置關鍵幀,滑鼠點來點去就可以了,Blend會自動幫我們生成我們想要的動畫效果. 第一步:要創建一個空的WPF項目 第二步:右鍵我們的項目,在最下方有一個,在B ...
  • Prism:框架介紹與安裝 什麼是Prism? Prism是一個用於在 WPF、Xamarin Form、Uno 平臺和 WinUI 中構建鬆散耦合、可維護和可測試的 XAML 應用程式框架 Github https://github.com/PrismLibrary/Prism NuGet htt ...
  • 在WPF中,屏幕上的所有內容,都是通過畫筆(Brush)畫上去的。如按鈕的背景色,邊框,文本框的前景和形狀填充。藉助畫筆,可以繪製頁面上的所有UI對象。不同畫筆具有不同類型的輸出( 如:某些畫筆使用純色繪製區域,其他畫筆使用漸變、圖案、圖像或繪圖)。 ...
  • 前言 嗨,大家好!推薦一個基於 .NET 8 的高併發微服務電商系統,涵蓋了商品、訂單、會員、服務、財務等50多種實用功能。 項目不僅使用了 .NET 8 的最新特性,還集成了AutoFac、DotLiquid、HangFire、Nlog、Jwt、LayUIAdmin、SqlSugar、MySQL、 ...
  • 本文主要介紹攝像頭(相機)如何採集數據,用於類似攝像頭本地顯示軟體,以及流媒體數據傳輸場景如傳屏、視訊會議等。 攝像頭採集有多種方案,如AForge.NET、WPFMediaKit、OpenCvSharp、EmguCv、DirectShow.NET、MediaCaptre(UWP),網上一些文章以及 ...
  • 前言 Seal-Report 是一款.NET 開源報表工具,擁有 1.4K Star。它提供了一個完整的框架,使用 C# 編寫,最新的版本採用的是 .NET 8.0 。 它能夠高效地從各種資料庫或 NoSQL 數據源生成日常報表,並支持執行複雜的報表任務。 其簡單易用的安裝過程和直觀的設計界面,我們 ...
  • 背景需求: 系統需要對接到XXX官方的API,但因此官方對接以及管理都十分嚴格。而本人部門的系統中包含諸多子系統,系統間為了穩定,程式間多數固定Token+特殊驗證進行調用,且後期還要提供給其他兄弟部門系統共同調用。 原則上:每套系統都必須單獨接入到官方,但官方的接入複雜,還要官方指定機構認證的證書 ...
  • 本文介紹下電腦設備關機的情況下如何通過網路喚醒設備,之前電源S狀態 電腦Power電源狀態- 唐宋元明清2188 - 博客園 (cnblogs.com) 有介紹過遠程喚醒設備,後面這倆天瞭解多了點所以單獨加個隨筆 設備關機的情況下,使用網路喚醒的前提條件: 1. 被喚醒設備需要支持這WakeOnL ...
  • 前言 大家好,推薦一個.NET 8.0 為核心,結合前端 Vue 框架,實現了前後端完全分離的設計理念。它不僅提供了強大的基礎功能支持,如許可權管理、代碼生成器等,還通過採用主流技術和最佳實踐,顯著降低了開發難度,加快了項目交付速度。 如果你需要一個高效的開發解決方案,本框架能幫助大家輕鬆應對挑戰,實 ...