- 產品:我想知道用戶到底有沒有看到過這個商品(DOM節點)?react
- 開發:這個簡單,我監聽頁面滾動,等商品出如今屏幕中,我就發個 埋點 記錄一下。git
- 產品:那你看這種呢,點旁邊的按鈕這個產品才顯示。github
- 開發:那我在 按鈕點擊事件 裏面發 埋點。bash
- 產品:其實咱這還有一種狀況,就是。。。服務器
- 開發 :好了!什麼都別說了,我 setInterval, 包你滿意!dom
- but. 做爲開發你能作的,遠不止於此。異步
接下來咱們會有三種實現方式,分別是:工具
1. 事件監聽ui
2. 定時器spa
3. IntersectionObserver
在開始以前,假設本身有一些工具方法
// 判斷dom元素是否在視口中
const isElementInViewPort = (el) => { /* 返回一個布爾值 */ }
// 把數據發送到服務器
const sendLog = (value) => { /**/ }複製代碼
最初的想法是使用window.scroll、window.resize進行全局監聽。對於特殊狀況,好比點擊出現的商品,則使用更特殊的處理方式。
注意:對於特殊的商品,要採用特殊的dom結構。所以會比較麻煩。
dom:
// 普通商品
<div data-expose="商品1">...</div>
<div data-expose="商品2">...</div>
// 點擊纔出現的商品
<div>...</div>
<buttton class="expose-click">顯示商品</button>複製代碼
js:
// 針對scroll
window.on('scroll', () => {
const elements = document.querySelectorAll('[data-expose]');
const elementList = [...elements];
elementList.forEach((element) => {
// 若是有發送成功的標記,則直接return
if(element.getAttribute('[has-exposed]')) return;
if (isElementInViewPort(element)) {
const logValue = element.getAttribute('[data-expose]')
sendLog(logValue)
// 曝光成功以後,則打一個標記
element.setAttribute('[has-exposed]', "1")
}
})
})
// 針對點擊
const target = document.querySelectorAll(".expose-click")
target.on('click', (e) => {
const target = e.target;
if(target.getAttribute('[has-exposed]')) return
const logValue = "點擊出現的商品"
sendLog(LogValue)
target.setAttribute('[has-exposed]', "1")
})複製代碼
由於方法1中,dom結構不一樣,而且方法不夠通用,因此思考新的方式。
使用setInterval,則不用考慮元素是怎麼出現的,只要出如今了屏幕中,則定時器會自動發現須要曝光的內容。
dom:
<div data-expose="商品1">...</div>
<div data-expose="商品2">...</div>
...
<div data-expose="點擊出現的商品">...</div>
<buttton>顯示商品</button>複製代碼
js:
const observeTimer = setInterval(() => {
const elements = document.querySelectorAll('[data-expose]');
const elementList = [...elements];
elementList.forEach((element) => {
// 若是有發送成功的標記,則直接return
if (element.getAttribute('[has-exposed]')) return;
if (isElementInViewPort(element)) {
const logValue = element.getAttribute('[data-expose]')
sendLog(logValue)
// 曝光成功以後,則打一個標記
element.setAttribute('[has-exposed]', "1")
}
})
}, 500)複製代碼
ok. 咱們的終極方案來了 IntersectionObserver。你們能夠看到以下這段話:
IntersectionObserver
接口提供了一種異步觀察目標元素與其祖先元素或頂級文檔視窗(viewport)交叉狀態的方法。
嗯。 嗯??這不就是專門用來作這個事情的嘛?
而後就看一下兼容性,着實通常。
可是w3c官方提供了polyfill (每一個提案 到 Working Draft階段一般會提供1-2個polyfill). 而後兼容性就能夠說是起飛了。
基本實現以下:
observerLibrary.js
let observer;
const init = (options) => {
//IntersectionObserver接受一個callback, 參數是全部被觀察的對象
return observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
let { target } = entry;
// 若是dom在視口中,則發送埋點
if(entry.isEntersecting) {
const logValue = target.getAttribute('data-expose')
sendLog(logValue)
target.setAttribute('has-exposed', "1")
// 中止監聽
observer.unobserve(target)
}
})
}, {thresholds: options.thresholds || 0, rootMargin: options.rootMargin || {}})
}
const getObserver = () => observer || init()
const observe = () => getObserver().observe;
const unobserve = () => getObserver().unobserve
const stopObserve = () => {
if(!observer) return;
observer.disconnect()
}
export default {
init,
observe,
unobserve,
stopObserve
}複製代碼
myObserver.js
import { init, observe, unobserve, stopObserve } from "observerLibrary"
// 須要一個機會去配置observer, 例如交叉比例達到多大時纔算曝光
const options = {}
init(options)
export default {
observe,
unobserve,
stopObserve
}複製代碼
component.js
import React from "react"
import { observe, unobserve } from "./myObserver"
const App = (props) => {
return <div>
<div data-expose="商品1" ref={observe}>...</div>
<div data-expose="商品2" ref={observe}>...</div>
</div>
}複製代碼
以上的實現應該完成了一個基礎的功能,可是會有如下問題:
所以,咱們能夠加強一下如今的實現:
最終的observe方式實現以下:
let subjectList = [];
const observe = (el) => {
if(!el) return;
// 過濾掉不存在在document上的節點
subjectList = filterObserveList(subjectList)
const newList = getElementWithExpose(el);
const newItemsNotInOldList = getNewItemsNotInOldList(observeList, newList);
observeList = addNewListToList(observeList, newList);
const observer = getObserver();
newItemsNotInOldList.forEach((item) => {
observer.observe(item);
})
}
/*
* 考慮到dom可能被意外移除,因此考慮當某個target已經不在document.body中的時候,執行unobserve(target)
* */
function filterObserveList(list = []) {
const contains = document.body.contains;
const observer = getViewabilityObserver();
const effectiveObserveList = [];
list.forEach((item, key) => {
if (contains(item)) {
effectiveObserveList.push(item);
} else {
observer.unobserve(item);
}
});
return effectiveObserveList;
};
function getElementWithExpose(el) {
if (!el) return [];
const children = Array.from(el.querySelectorAll(`[data-expose]`));
const isIncludeSelf = !!el.getAttribute('data-expose');
const rawResult = isIncludeSelf ? [...children, el] : children;
return rawResult.filter(item => !item.getAttribute('has-exposed');
};
function getNewItemsNotInOldList(list = [], newList = []) {
return newList.filter(item => !list.includes(item))
};
function addNewListToList(list = [], newList = []) {
return Array.from(new Set([...list, ...newList]))
};複製代碼
此外,還有一些其餘注意事項,須要使用者去特殊處理。好比:
完整代碼能夠訪問 github。