// 监听的对象(组件内部需要使用组件的this属性)
context: {
type: Function,
value: null,
},
// 选择器
/**
selector类似于 CSS 的选择器,但仅支持下列语法。
ID选择器:#the-id
class选择器(可以连续指定多个):.a-class.another-class
子元素选择器:.the-parent > .the-child
后代选择器:.the-ancestor .the-descendant
跨自定义组件的后代选择器:.the-ancestor >>> .the-descendant
多选择器的并集:#a-node, .some-other-nodes
*/
selector: {
type: String,
},
// 相交区域,默认为页面显示区域
relativeTo: {
type: String,
value: null,
},
// 是否同时观测多个目标节点,动态列表不会自动增加
observeAll: {
type: Boolean,
value: false,
},
// 是否只观测一次
once: {
type: Boolean,
value: CONFIG.DEFAULT_EXPOSURE_ONCE,
},
// 曝光时长,满足此时长记为曝光一次,单位ms
exposureTime: {
type: Number,
value: CONFIG.DEFAULT_EXPOSURETIME,
},
// 成功曝光后间隔时长,在此时长内不进行观测,单位ms
interval: {
type: Number,
value: CONFIG.DEFAULT_EXPOSURE_INTERVAL,
},
},
lifetimes: {
ready: function () {
if (!this.data.selector) return;
this.ob = new IntersectionObserver({
context: this.data.context ? this.data.context() : null,
selector: this.data.selector,
relativeTo: this.data.relativeTo,
observeAll: this.data.observeAll,
once: this.data.once,
interval: this.data.interval,
exposureTime: this.data.exposureTime,
onFinal: (startTime, endTime) => {
const { event, project, properties } = this.data;
let time = formatDate(new Date(), ‘YYYY-MM-DD hh:mm:ss.S’);
time =
time.length >= 19
-
? time.slice(0, 19)
-
${time.slice(0, 17)}0${time.slice(17, 18)}
;
appendQueue(‘exposure’, {
time,
event,
project,
properties: {
…properties,
startTime,
endTime,
},
});
},
});
this.ob.connect();
},
detached: function () {
// 在组件实例被从页面节点树移除时执行
this.ob.disconnect();
},
},
pageLifetimes: {
show: function () {
// 所在页面被展示
if (this.ob) this.ob.connect();
},
hide: function () {
// 所在页面被隐藏
if (this.ob) this.ob.disconnect();
},
},
});
import CONFIG from ‘./config’;
export default class IntersectionObserver {
constructor(options) {
this.$options = {
context: null,
selector: null,
relativeTo: null,
observeAll: false,
initialRatio: 0,
// 露出比例
threshold: CONFIG.DEFAULT_THRESHOLD,
once: CONFIG.DEFAULT_EXPOSURE_ONCE,
exposureTime: CONFIG.DEFAULT_EXPOSURETIME,
interval: CONFIG.DEFAULT_EXPOSURE_INTERVAL,
// 满足曝光后回调
onFinal: () => null,
…options,
};
this.$observer = null;
this.startTime = null;
this.isIntervaling = false;
this.stopObserving = false;
this.neverObserving = false;
}
connect() {
this.stopObserving = false;
if (this.$observer || this.isIntervaling || this.neverObserving) return;
this.$observer = this._createObserver();
}
reconnect() {
this.disconnect();
this.connect();
}
disconnect() {
this.stopObserving = true;
if (!this.$observer) return;
this.$observer.disconnect();
this.$observer = null;
// 断开连接,即停止浏览,判断是否上报
if (!this.startTime) return;
this._judgeExposureTime();
}
_createObserver() {
const opt = this.$options;
const observerOptions = {
thresholds: [opt.threshold],
observeAll: opt.observeAll,
initialRatio: opt.initialRatio,
};
// 创建监听器
const ob = opt.context
-
? opt.context.createIntersectionObserver(observerOptions)
- wx.createIntersectionObserver(null, observerOptions);
// 相交区域设置
if (opt.relativeTo) ob.relativeTo(opt.relativeTo);
else ob.relativeToViewport();
// 开始监听
ob.observe(opt.selector, (res) => {
const { intersectionRatio } = res;
const visible = intersectionRatio >= opt.threshold;
if (visible && !this.startTime) {
this.startTime = new Date();
}
if (!visible && this.startTime) {
this._judgeExposureTime();
}
});
return ob;
}
_judgeExposureTime() {
const endTime = new Date();
const lastTime = endTime.getTime() - this.startTime.getTime();
if (lastTime < this.$options.exposureTime) {
this.startTime = null;
console.log(‘曝光时间不足’, lastTime / 1000);
return;
}
console.log(‘曝光时间足够’, lastTime / 1000);
this.$options.onFinal(this.startTime, endTime);
this.startTime = null;
if (this.$options.once) {
this.neverObserving = true;
if (this.$observer) {
this.$observer.disconnect();
this.$observer = null;
}
}
if (this.$options.interval) {
if (this.$observer) {
this.$observer.disconnect();
this.$observer = null;
}
this.isIntervaling = true;
setTimeout(() => {
this.isIntervaling = false;
if (!this.stopObserving) this.connect();
}, this.$options.interval);
}
}
}
页面曝光埋点
由于小程序页面有很多生命周期,因此我们可以借助 onShow,onHide 来实现检测页面的显示和关闭。
appendQueue
一些场景下我们没法绑定事件到 dom 上,比如小程序的分享,针对这种场景我们提供了 appendQueue 方法,把埋点加入到缓冲队列中。
参考网易云方案,我们也采用定时任务上报,点击类上报频率 1000ms,曝光类 3000ms,不过相较于网易云,我们采用了两种方案减少埋点数据丢失的可能性。
-
进入队列的同时保存到 localStorage,上报后自动删除,避免在上报间隙用户关闭小程序导致埋点数据丢失。
-
小程序 APP 的 onHide 生命周期立即上报,减少由于用户清理小程序缓存导致埋点数据丢失。
import { multiReport } from ‘./report’;
import CONFIG from ‘./config’;
let exposureQueue = [];
let clickQueue = [];
let isCollectingExposure = false;
let isCollectingClick = false;
const appendQueue = (action, track) => {
action === ‘exposure’ ? exposureQueue.push(track) : clickQueue.push(track);
report(action);
wx.setStorage({
key: ${action}Queue
,
data: JSON.stringify(action === ‘exposure’ ? exposureQueue : clickQueue),
});
};
const clearQueue = (action) => {
action === ‘exposure’ ? (exposureQueue = []) : (clickQueue = []);
wx.removeStorage({ key: ${action}Queue
});
};
const report = (action) => {
const delay =
action === ‘exposure’ ? CONFIG.EXPOSURE_DELAY : CONFIG.CLICK_DELAY;
if (action === ‘exposure’ ? isCollectingExposure : isCollectingClick) return;
action === ‘exposure’
-
? (isCollectingExposure = true)
- (isCollectingClick = true);
const queue = action === ‘exposure’ ? exposureQueue : clickQueue;
if (queue.length !== 0) {
setTimeout(() => {
multiReport(queue);
action === ‘exposure’
-
? (isCollectingExposure = false)
- (isCollectingClick = false);
clearQueue(action);
}, delay);
}
};
const reportImediately = () => {
multiReport(exposureQueue).then(() => {
isCollectingExposure = false;
clearQueue(‘exposure’);
});
multiReport(clickQueue).then(() => {
isCollectingClick = false;
clearQueue(‘click’);
});
};
export { appendQueue, reportImediately };
点击埋点
注意事项: 包裹的元素最外层需要有个 container
<tracker-click
event=“ClickIcon”
project=“test”
properties=‘{“iconName”:“{{item.text}}”}’
曝光埋点
注意事项:
-
包裹的元素最外层需要有个 container
-
如果是动态加载列表 selector 需要绑定每一个元素 id,如果不是动态加载列表,可以绑定 class 结合 observeAll 使用。
-
组件内部使用曝光埋点,需要传递父元素 this 给曝光埋点组件
<tracker-exposure
selector=“#menuIcon{{index}}”
event=“ViewIcon”
project=“test”
properties=‘{“iconName”:“{{item.text}}”}’
<tracker-exposure
selector=“#banner{{index}}”
event=“ViewBanner”
project=“test”
properties=‘{“bannerId”:“{{item.id}}”}’
context=“{{context}}”
总结
技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。
曝光埋点
注意事项:
-
包裹的元素最外层需要有个 container
-
如果是动态加载列表 selector 需要绑定每一个元素 id,如果不是动态加载列表,可以绑定 class 结合 observeAll 使用。
-
组件内部使用曝光埋点,需要传递父元素 this 给曝光埋点组件
<tracker-exposure
selector=“#menuIcon{{index}}”
event=“ViewIcon”
project=“test”
properties=‘{“iconName”:“{{item.text}}”}’
<tracker-exposure
selector=“#banner{{index}}”
event=“ViewBanner”
project=“test”
properties=‘{“bannerId”:“{{item.id}}”}’
context=“{{context}}”
总结
技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。