我們是袋鼠雲數棧 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列表,若列表中有未完成曝光且符合曝光時長規則的元素,則觸發其曝光事件,並更新列表中曝光信息。
初始化流程
元素髮生掛載或卸載過程
元素曝光過程
代碼實現
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
- 大數據分散式任務調度系統——Taier
- 輕量級的 Web IDE UI 框架——Molecule
- 針對大數據領域的 SQL Parser 項目——dt-sql-parser
- 袋鼠雲數棧前端團隊代碼評審工程實踐文檔——code-review-practices
- 一個速度更快、配置更靈活、使用更簡單的模塊打包器——ko
- 一個針對 antd 的組件測試工具庫——ant-design-testing