一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。
现状
为准确分析各前端页面实际对用户的吸引力,需要统计的页面元素的曝光数据。曝光的含义比较模糊,具体的统计方式也比较麻烦,本文分享一个前端曝光埋点上报的实现方案。
方案
为了统计曝光数据,首先要做的是,定义什么是曝光,然后制定上报数据的策略。
根据我们业务的实际情况,我们可以这样设计:
- 曝光定义:dom元素出现在屏幕窗口中,被用户看到,且停留时间超过500ms,才算一次曝光。dom元素退出窗口后重新进入窗口,再停留500ms,记为第二次曝光。
- 数据上报:需要尽量减少上报次数(1)定时器每N秒检查一次,如果有待上报数据就请求接口上报(2)如果待上报数据大于M条,直接上报,不需要等待N秒。
开始操作
整体实现
具体的代码实现如下:
- 使用IntersectionObserver观察是否出现和消失在窗口,用IntersectionObserver polyfill提升兼容性。
- 用vue的指令,实现上报数据的绑定,最后使用的时候,只需要为需要上报的元素,加上v-treport=“上报的数据”。
- 在指令绑定的时候,为dom元素绑定report-data和guid属性,具体值分别为待上报数据和唯一ID。
- 具体观测和上报曝光的逻辑,后面具体讲。
使用方式
绑定指令后的元素:
具体细节
元素X进入窗口
- 元素X进入窗口,记录到sessionStorage的to-observe队列(如果已存在,就不加入队列)(使用sessionStorage,是因为,浏览器关闭了不在需要计算观测结果)
- 结构为 {stime:观测到的时间, id:guid, data:待上报数据,hasObserve:false}
元素X退出窗口
- 从to-observe队列获取X的stime,如果(当前时间-stime)>=500ms而且hasObserve为false,将X元素的数据推入localStorage的to-report的队列(使用localStorage,浏览器关闭了,在下次进来,还可以把to-report未上报的进行上报)。
- 无论何种情况,元素X都要推出to-observe队列。
曝光定时器(每500ms检查一次)
- 如果to-observe队列中存在(当前时间-stime)>=500ms的X,hasObserve置为true,将X元素的数据推入to-report的队列
上报定时器(每1000ms检查一次)
- 如果to-report队列存在记录,上报并从to-report移出。
- 如果推入to-report队列时,队列长度大于M,直接上报。
观测元素的几种情况:
- A:进入窗口,500ms后退出窗口,需要上报
- B:进入窗口,没有退出窗口,超过了500ms,需要上报
- C:进入窗口,不到500ms退出窗口,不需要上报
代码实现
require('intersection-observer');
const MIN_OBSERVE_TIME = 500;
const OBSERVE_REPEAT_TIME = 1000;
const REPORT_REPEAT_TIME = 1000;
// 获取IntersectionObserver的单例
class ReportObserver {
constructor() {
this.instance = null;
this.intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
handleEnter(entry);
} else {
handleExit(entry);
}
});
});
// 初始化定时器
initInterval();
}
observe(el, reportData) {
el.setAttribute('report-data', JSON.stringify(reportData));
el.setAttribute('guid', guid());
this.intersectionObserver.observe(el);
}
// 获取IntersectionObserver的实例
static getInstance() {
if (!this.instance) {
this.instance = new ReportObserver();
}
return this.instance;
}
}
// 元素X进入窗口,记录到sessionStorage的to-observe队列(如果已存在,就不加入队列),
// 结构为 {stime:观测到的时间, id:元素ID, data:待上报数据,hasObserve:false}。
const handleEnter = function (entry) {
const dom = entry.target;
const data = dom.getAttribute('report-data');
const id = dom.getAttribute('guid');
const stime = new Date().getTime();
const hasObserve = false;
const observeData = { id, data, stime, hasObserve };
if (!findToObserve(id)) {
pushToObserve(observeData);
}
};
// 元素X退出窗口:
// 1、从to-observe队列获取X的stime,如果(当前时间-stime)>=500ms而且hasObserve为false,将X元素的数据推入to-report的队列。
// 2、无论何种情况,元素X都要推出to-observe队列。
const handleExit = function (entry) {
const dom = entry.target;
const id = dom.getAttribute('guid');
const etime = new Date().getTime();
const value = findToObserve(id);
if (value && etime - value.stime >= MIN_OBSERVE_TIME && !value.hasObserve) {
pushToReport(value);
}
deleteFromToObserve(id);
};
// 初始化定时器
const initInterval = function () {
// 曝光定时器
setInterval(() => {
// 如果to-observe队列中存在(当前时间-stime)>=500ms并且hasObserve为false的X,将X的hasObserve置为true,并推入to-report的队列
toObserveList().forEach((value) => {
const etime = new Date().getTime();
if (etime - value.stime >= MIN_OBSERVE_TIME && !value.hasObserve) {
value.hasObserve = true;
pushToObserve(value);
pushToReport(value);
}
});
}, OBSERVE_REPEAT_TIME);
// 上报定时器
setInterval(() => {
// 如果to-report队列存在记录,上报并从to-report移出。
if (toReportList.length) {
postReportData(toReportList);
clearToReport();
}
}, REPORT_REPEAT_TIME);
};
const postReportData = function (dataList) {
// 调用后台接口上报数据
};
export default {
bind(el, binding) {
if (
'IntersectionObserver' in window
&& 'IntersectionObserverEntry' in window
&& 'intersectionRatio' in window.IntersectionObserverEntry.prototype
) {
// 开始监听
ReportObserver.getInstance().observe(el, binding.value);;
}
},
};