埋点方案介绍
首先曝光埋点是指记录某个商品(dom元素)出现在指定视图下的频率,平台可通过大数据分析出用户的习惯,从而达到增加用户的购买力的效果。
dom滑动至可视区域,印入脑海的第一种方案是监听滚动事件,通过Element.getBoundingClientRect() 计算目标元素与视图的位置,然而现实是残酷的,且不说getBoundingClientRect Api会引起回流,造成性能问题(当然这个是主要问题),要计算目标元素的位置,对于我这种伸手党的前端开发来说也是很恶心的好吧。
苦苦搜寻其他方案,当我看到Intersection Observer API的时候,似乎看到了希望,话不多说直接上图,其中还说明Element.getBoundingClientRect() 的性能问题。
Intersection Observer API 用法
var options = {
root: document.querySelector('#scrollArea'), // 根dom元素, null 默认为浏览器视图
rootMargin: '0px', // root元素的外边距
threshold: 1.0 // 目标元素与根元素相交程度触发cb - [0 - 1]
}
// 创建一个observer 实例
var observer = new IntersectionObserver(callback, options);
// 目标元素
var target = document.querySelector('#listItem');
observer.observe(target);
详情用法请参考 Intersection Observer API - Web API 接口参考 | MDN
兼容性解决方案请参考 IntersectionObserver polyfill
项目实践
实现思路
- 通过new IntersectionObserver() 初始化一个全局observer实例.
- 找到目标dom元素,通过observer.observe(element),将目标dom节点添加至观察列表中。
- 考虑一下延迟上报,针对列表无限下拉,导致并发问题。我们通过一个构建dataList数组,存储曝光的上报数据,并设置maxNum,如果曝光的数量达到maxNum 直接上报,如果没有达到开启一个定时器,定时上报。
- 每次上报之后,需要把数据从dataList 中移除,避免重复上报。
代码实现
- 封装一个Exposure 类,并暴露出一个 exposure实例
// polyfill 解决兼容性问题
import 'intersection-observer';
// 延迟时间,节流作用
IntersectionObserver.prototype['THROTTLE_TIMEOUT'] = 300;
class Exposure {
constructor(maxNum = 10) {
this.dataList = [];
this.maxNum = maxNum;
this.time = 0; // 延迟上报时间
this.dataList = [];
this.maxNum = maxNum; // 一次上报最大个数
this._observer = null;
this._timer = null;
this.init();
}
// 初始化
init() {
const self = this;
this._observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) { // 进入视图触发
try {
self._timer && clearTimeout(self._timer); // 清除定时器
const eventParam = entry.target.attributes['data-param'].value;
const eventId = entry.target.attributes['data-eventId'].value;
self.dataList.push({ eventId, eventParam });
// 已经上报的节点、取消对该DOM的观察
self._observer.unobserve(entry.target);
// 超出最大长度直接上报
if (self.dataList.length >= self.maxNum) {
self.send();
} else if (self.dataList.length > 0) {
self._timer = setTimeout(() => { // 定时上报
self.send();
}, self.time);
}
} catch (err) {
console.log(err);
}
}
})
}, {
root: document.querySelector('#app'),
rootMargin: "0px",
threshold: 1 // 目标dom出现在视图的比例 0 - 1
});
}
// 添加至观察列表
add(entry) {
const { el } = entry || {};
this._observer && this._observer.observe(el)
}
// 触发上报数据
send() {
console.log('---上报埋点数据---');
const data = this.dataList.slice(0, this.maxNum);
window.ZM_JSSDK && window.ZM_JSSDK.sendStuEvent(data)
}
// 组件销毁,数据全部上报
beforeUnmount() {
console.log('---离开页面,上报埋点数据---');
const data = this.dataList;
window.ZM_JSSDK && window.ZM_JSSDK.sendStuEvent(data)
}
}
export default new Exposure();
- 封装 React HOC
import React, { useEffect } from "react";
import exposure from "../../utils/exposure";
// 这里使用hoc,目的在于属性封装
const exposureHoc = (WrappedComponent) => (props) => {
const { index, eventId, sendData = true, eventParam } = props;
// did mount 添加至观察列表
useEffect(() => {
sendData && exposure.add({ el: document.getElementById(`paper-content-${index}`) })
}, []);
const value = { index: index, ...eventParam };
// 通过dom data 属性设置埋点参数
return (
<div id={`paper-content-${index}`} data-eventId={eventId} data-param={JSON.stringify(value)}>
<WrappedComponent {...props} />
</div>
)
}
export default exposureHoc;
- 使用
// BookItem 组件
import React from 'react';
import exposureHoc from 'components/Exposure';
const BookItem = (props) => {
const { index, subItem } = props;
return (
<div key={subItem.bookId}>
<img src={subItem.cover} width='136px' height='178px' />
<span>{subItem.bookName}</span>
</div>
)
}
export default exposureHoc(BookItem);
// 父组件
<BookItem
index={index}
subItem={subItem}
eventId='07_learnmaterial_materiallist_show' // 埋点事件ID
eventParam={{ ... }} // 埋点参数
/>
总结
以上就是我本次针对前端曝光埋点的实现方案,可以根据业务继续封装,如有问题请多多交流。