如何實現(xiàn)元素的曝光監(jiān)測

一些名詞解釋

曝光
頁面上某一個元素砌溺、組件或模塊被用戶瀏覽了菇曲,則稱這個元素、組件或模塊被曝光了抚吠。
視圖元素
將頁面上展示的元素常潮、組件或模塊統(tǒng)稱為視圖元素。
可見比例
視圖元素在可視區(qū)域面積/視圖元素整體面積楷力。
有效停留時長
視圖元素由不可見到可見喊式,滿足可見比例并且保持可見狀態(tài)的持續(xù)的一段時間孵户。
重復(fù)曝光
在同一頁面,某個視圖元素不發(fā)生DOM卸載或頁面切換的情況下岔留,發(fā)生的多次曝光稱為重復(fù)曝光夏哭。例如頁面上某個視圖元素,在頁面來回滾動時献联,則會重復(fù)曝光竖配。

如何監(jiān)測曝光

需要考慮的一些問題

曝光條件
頁面上某一視圖元素的可見比例達到一定值(例如0.5),且有效停留時間達到一定時長(例如500ms),則稱該視圖元素被曝光了里逆。
如何檢測可見比例
使用 IntersectionObserver api 對元素進行監(jiān)聽进胯,通過 threshold 配置項設(shè)置可見比例,當達到可見比例時原押,觀察器的回調(diào)就會執(zhí)行胁镐。
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);

如何監(jiān)聽動態(tài)元素
使用 IntersectionObserver 對元素進行監(jiān)聽之前,需要先獲取到元素的 DOM诸衔,但對于一些動態(tài)渲染的元素盯漂,則無法進行監(jiān)聽。所以笨农,需要先監(jiān)聽DOM元素是否發(fā)生掛載或卸載就缆,然后對元素動態(tài)使用IntersectionObserver 進行監(jiān)聽,可以使用 MutationObserver 對 DOM變更進行監(jiān)聽谒亦。
MutationObserver的使用示例:

// 選擇需要觀察變動的節(jié)點
const targetNode = document.getElementById("some-id");

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

// 當觀察到變動時執(zhí)行的回調(diào)函數(shù)
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.");
    }
  }
};

// 創(chuàng)建一個觀察器實例并傳入回調(diào)函數(shù)
const observer = new MutationObserver(callback);

// 以上述配置開始觀察目標節(jié)點
observer.observe(targetNode, config);

// 之后违崇,可停止觀察
observer.disconnect();

如何監(jiān)聽停留時長
維護一個觀察列表,元素可見比例滿足要求時诊霹,將該元素信息(包含曝光開始時間)添加到列表羞延,當元素退出可視區(qū)域時(可見比例小于設(shè)定值),用當前時間減去曝光開始時間脾还,則可獲得停留時長伴箩。

總體實現(xiàn)

實現(xiàn)一個exposure方法,支持傳入需要檢測曝光的元素信息(需包含className)鄙漏,使用 IntersectionObserver 和 MutationObserver 對元素進行動態(tài)監(jiān)聽嗤谚。

  • 初始化時,根據(jù)className查找出已渲染的曝光監(jiān)測元素怔蚌,然后使用IntersectionObserver統(tǒng)一監(jiān)聽巩步,如果有元素發(fā)生曝光,則觸發(fā)對應(yīng)曝光事件桦踊;
  • 對于一些動態(tài)渲染的曝光監(jiān)測元素椅野,需要使用MutationObserver監(jiān)聽dom變化。當有節(jié)點新增時,新增節(jié)點若包含曝光監(jiān)測元素竟闪,則使用IntersectionObserver進行監(jiān)聽离福;當有節(jié)點被移除時,移除節(jié)點若包含曝光監(jiān)測元素炼蛤,則取消對其的監(jiān)聽妖爷;
  • 維護一個observe列表,元素開始曝光時將元素信息添加到列表理朋,元素退出曝光時如果曝光時長符合規(guī)則絮识,則觸發(fā)對應(yīng)曝光事件,并在observe列表中將該元素標記為已曝光嗽上,已曝光后再重復(fù)曝光則不進行采集次舌。如果元素在DOM上被卸載,則將該元素在observe列表中的曝光事件刪除炸裆,下次重新掛載時垃它,則重新采集鲜屏。
  • 設(shè)置一個定時器烹看,定時檢查observe列表,若列表中有未完成曝光且符合曝光時長規(guī)則的元素洛史,則觸發(fā)其曝光事件惯殊,并更新列表中曝光信息。

初始化流程

file

元素發(fā)生掛載或卸載過程

file

元素曝光過程

file

代碼實現(xiàn)

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) {
          // 若已經(jīng)曝光過也殖,則不進行采集
          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) {
              // 觸發(fā)曝光事件土思,并更新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隊列忆嗜,若隊列中有符合曝光時長規(guī)則的元素己儒,則修改曝光狀態(tài),并觸發(fā)曝光事件捆毫。
    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;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闪湾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子绩卤,更是在濱河造成了極大的恐慌途样,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件濒憋,死亡現(xiàn)場離奇詭異何暇,居然都是意外死亡,警方通過查閱死者的電腦和手機凛驮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門裆站,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事遏插∥婊撸” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵胳嘲,是天一觀的道長厂僧。 經(jīng)常有香客問我,道長了牛,這世上最難降的妖魔是什么颜屠? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮鹰祸,結(jié)果婚禮上甫窟,老公的妹妹穿的比我還像新娘。我一直安慰自己蛙婴,他們只是感情好石咬,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布霸奕。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪巴帮。 梳的紋絲不亂的頭發(fā)上邢享,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天侨赡,我揣著相機與錄音摇庙,去河邊找鬼。 笑死絮姆,一個胖子當著我的面吹牛醉冤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播篙悯,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼蚁阳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鸽照?” 一聲冷哼從身側(cè)響起螺捐,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎移宅,沒想到半個月后归粉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡漏峰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年糠悼,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浅乔。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡倔喂,死狀恐怖铝条,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情席噩,我是刑警寧澤班缰,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站悼枢,受9級特大地震影響埠忘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜馒索,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一莹妒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绰上,春花似錦旨怠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至百揭,卻和暖如春爽哎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背信峻。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工倦青, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓮床,地道東北人盹舞。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像隘庄,于是被迫代替她去往敵國和親踢步。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容