virtualHost: true
},
/**
- 组件的属性列表
*/
properties: {
event: {
type: String,
value: ‘Click’,
},
project: {
type: String,
value: ‘’,
},
properties: {
type: Object,
value: null,
},
},
/**
- 组件的方法列表
*/
methods: {
trackerClick() {
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(‘click’, {
time,
event,
project,
properties,
});
},
},
});
曝光埋点
曝光埋点是最难实现的埋点,需要满足以下三点:
-
元素出现在视窗内一定的比例才算一次合法的曝光
-
元素在视窗内停留的时长达到一定的标准才算曝光
-
统计元素曝光时长
微信官方文档 – IntersectionObservers
由于微信小程序已支持 IntersectionObservers API,因此我们采用这个 API 实现曝光埋点。
不过由于小程序不具有 DOM 结构,没法像 REACT 传递 DOM 元素实现曝光埋点,并且组件内部的使用有一些需要特别注意的点。
import IntersectionObserver from ‘…/…/…/utils/report/intersection-observer’;
import { appendQueue } from ‘…/…/…/utils/report/reportQueue’;
import { formatDate } from ‘…/…/…/utils/formatDate’;
import CONFIG from ‘…/…/…/utils/report/config’;
Component({
/**
- 虚拟化组件节点,防止影响 css 样式以及不增加 DOM 层级
*/
options: {
virtualHost: true
},
/**
- 组件的属性列表
*/
properties: {
/**
- 上报的参数
*/
event: {
type: String,
value: ‘View’,
},
project: {
type: String,
value: ‘baoyanmp’,
},
properties: {
type: String,
},
/**
- IntersectionObserver属性
*/
// 监听的对象(组件内部需要使用组件的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;
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
exposure’ ? isCollectingExposure : isCollectingClick) return;
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-CYQLeRt2-1715244148097)]
[外链图片转存中…(img-IXBolqCL-1715244148098)]
[外链图片转存中…(img-SLWm2s3C-1715244148098)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!