如何實現元素曝光上報

進行數據上報的時候,常常會遇到列表數據曝光上報的問題,只對在當前可視範圍內的數據內容進行曝光上報,而對於未在可視範圍內的數據不進行曝光上報,等待用戶滾動頁面或者區域使元素出如今可視範圍內時才進行曝光上報。node

解決方案

目前針對此類問題,主要有兩種解決方案。npm

方案一:監聽頁面或者區域scroll事件,經過getBoundingClientRect接口取元素的位置與可視窗口進行判斷。

function isElementInViewport(el) {
    var rect = el.getBoundingClientRect();

    var width_st = rect.width / 2,
        height_st = rect.height / 2;

    var innerHeight = window.innerHeight,
        innerWidth  = window.innerWidth;


    if (   rect.top <=0 && rect.height > innerHeight 
        || rect.left <= 0 && rect.width > innerWidth
    ) {
        return rect.left * rect.right <= 0
            || rect.top * rect.bottom <= 0
    }

    return (
           rect.height > 0 
        && rect.width  > 0 
        &&  (  ( rect.top  >= 0 && rect.top <= innerHeight - height_st )
            || ( rect.bottom  >= height_st && rect.bottom <= innerHeight ) )
        &&  (  ( rect.left >= 0 && rect.left <= innerWidth - width_st  )
            || ( rect.right >= width_st && rect.right <= innerWidth  ) )
    );
}

var nodes = document.querySelectorAll(".item")
function report(node) {
    // 上報的邏輯
}
window.onscroll = function() {
    nodes.forEach(node => {
        if( isElementInViewport(node) ) {
            report(node)
        }
    })
    
}

優勢:兼容性好
缺點:dom

  • 須要關注頁面或者區域的scroll事件
  • 頻繁的scroll事件,性能問題

方案二:經過 IntersectionObserver 監聽元素是否處於可視範圍

function report(node) {
    // 上報的邏輯
}
var intersectionObserver = new IntersectionObserver(entries => {
    entries.forEach(entry => {
        if( entry.intersectionRatio > 0 ) {
            report(entry.target)
        }
    })
})
var nodes = document.querySelectorAll(".item")
nodes.forEach(node => {
    intersectionObserver.observe(node)
})

優勢:異步

  • 無須關注 scroll
  • 回調是異步觸發,不會頻繁觸發,性能好

缺點:兼容性很差?性能

實際上,針對兼容性問題,w3c 官方提供了對應 polyfill, 所以intersectionObserver用於生產是可行的。ui

總結

筆者在實際運用中,經過 IntersectionObserver 封裝了一個簡單的調用庫,應用於可視化埋點 sdk 中,用於解決元素曝光問題,以下this

require('intersection-observer'); // polyfill

class Exposure {
    constructor(callback) {
        if (!callback || typeof callback !== 'function') {
            throw new Error("need callback or selector param")
            return
        }
        this.intersectionObserver = new IntersectionObserver((entries) => {
            entries.forEach(item => {
                if (item.intersectionRatio > 0) {
                    if (item.target) {
                        callback(item.target, item)
                        this.intersectionObserver.unobserve(item.target)
                    }
                }
            })
        });
    }

    observe(selector, ignoreExposured) {
        if (!this.intersectionObserver || !selector) {
            return
        }
        let nodes = []
        if( this.isDOM(selector) ) { // dom節點
            nodes = [selector]
        }else { // 選擇器
            nodes = document.querySelectorAll(selector)
        }
        if (!nodes.length) {
            return
        }
        nodes.forEach(node => {
            if (!ignoreExposured && node.__wg__tracker__exposured__) {
                return
            }
            node.__wg__tracker__exposured__ = true
            // 開始觀察
            this.intersectionObserver.observe(
                node
            );
        })
    }

    disconnect() {
        if (!this.intersectionObserver) {
            return
        }
        this.intersectionObserver.disconnect()
    }

    isDOM(obj) {
        if( !obj ) {
            return false
        }
        if( typeof HTMLElement === 'object' ) {
            return obj instanceof HTMLElement
        }
        if( typeof obj === 'object'  && obj.nodeType === 1 && typeof obj.nodeName === 'string' ) {
            return true
        }
        return false
    }
}

export default  Exposure

調用方法:code

function report() {}
var exposurer = new Exposure((node) => {
    report(node)
})
exposurer.observe(".item)
相關文章
相關標籤/搜索