什么是数据埋点
所谓数据埋点就是应用在规定流程中 对特定行为或事件进行数据采集
。使用采集的数据做用户分析和页面分析,可以获得应用的总体使用情况,为后续优化产品和运营提供数据支撑。常见数据埋点内容包括:访问量、停留时长、曝光量、点击量、跳出率等等。
微信小程序也为我们提供了自定义分析统计,其中包括 API 上报(代码埋点),填写配置(无埋点,只需在公众后台配置)。而第三方统计平台比较有名的就是阿拉丁统计,只需引入集成的 SDK,开发成本低,能够满足大部分的需求。
数据埋点需要分析页面流程,确定埋点需求,选择埋点方式。如果是代码埋点,主要关注触发时机、条件判断、捕获数据,其次要注意是否有遗漏的场景没有做到埋点。代码埋点虽然成本较大(侵入代码),但是精准度较高,能够很好的满足埋点需求。
什么是曝光量
曝光量顾名思义是 指定元素出现在可观察视图内的次数
,也可以理解为展示量。
通常我们会使用 点击量 / 曝光量
得出 点击率
,作为衡量一个内容是否受用户喜爱的指标之一。比如,曝光 100 次只有 10 人点击,和曝光 100 次 有 100 个人点击,很明显后者更受用户喜爱。利用这些数据参考,可以推荐更多用户喜爱的内容,以此来留住用户。
交叉观察者
IntersectionObserver
接口,提供了一种异步观察 目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态
的方法,祖先元素与视窗(viewport)被称为根(root)。简单来说就是,观察的目标是否和祖先元素和视窗发生交叉,即进入或离开。
小程序从基础库 1.9.3
开始支持 wx.createIntersectionObserver
接口(组件内使用 this.createIntersectionObserver
),使用此接口可创建 IntersectionObserver对象
。对此接口不了解的可以查看官方文档。
基础使用
// 创建实例
let ob = this.createIntersectionObserver()
// 相对于文档视窗监听
ob.relativeToViewport()
.observe('.box', res => {
// res.intersectionRatio 为相交比例
if (res.intersectionRatio > 0) {
console.log('进入页面')
} else {
console.log('离开页面')
}
})
![91f43d6aae95738a6583b5a406b7fda4.gif](https://i-blog.csdnimg.cn/blog_migrate/aa73337fcf60d52a700bb7c6a0e87559.gif)
阈值
在创建实例时可以传入一些配置,其中 thresholds
(阈值)是比较重要的一项配置,它可以控制触发回调的时机。thresholds
是一个数字类型的数组,默认为 [0]
。即相交比例为 0
时触发一次回调,下面我们来设置阈值,看看会有什么改变:
// 创建实例
let ob = this.createIntersectionObserver({
thresholds: [0, 0.5, 1]
})
![d0295717edcde8ec487c2ec242d7e821.gif](https://i-blog.csdnimg.cn/blog_migrate/7dca4ff2363f742d1e62c9ebc5849d53.gif)
从图上可以看到,元素在相交比例为 0
、 0.5
、 1
都各自触发了一次回调。在统计曝光量设置阈值非常有用,通常我会设置为 1
,表示元素要完全展示在页面上才会进行记录,这样数据会更加真实准确。
收缩和扩展参照区域
除了阈值之外还有另一项重要的设置,在使用 relativeTo
或 relativeToViewport
规定参照区域时,我们可以传入配置 margins
来收缩和扩展参照区域。margins
包括 left
、 right
、 top
、 bottom
四个参数配置。
// 创建实例
let ob = this.createIntersectionObserver()
// 相对于文档视窗监听
ob.relativeToViewport({
bottom: -330
})
.observe('.box', res => {
// res.intersectionRatio 为相交比例
if (res.intersectionRatio > 0) {
console.log('进入页面')
} else {
console.log('离开页面')
}
})
上面将参照区域底部收缩 330px,可以理解为整体的区域从底部开始被裁剪 330px,因此元素只有进入页面上半区才会触发回调。
![efe646cf411e2a51cbe6d4d8763c0014.gif](https://i-blog.csdnimg.cn/blog_migrate/57cf4bd107e11ee7441e7f49b09e6313.gif)
进入正题
经过以上一些介绍,相信大家对交叉观察者的好处和使用都了解的差不多。接下来进入正题 ~
背景
此次我做的项目是资讯类目的小程序,主要用于发布和转载一些学术文章。对于这种资讯的项目,需要通过数据埋点来收集用户的阅读习惯,以此来为用户推荐文章。
埋点方面用微信后台提供的自定义分析以文章为单位进行收集,而我们自己后台会以用户为单位进行收集。前者得出整体用户阅读偏好和文章热度,后者主要精确到用户,分析用户单位的阅读偏好。
改造组件
在分析页面布局和pm的商讨后,多处需要统计曝光量的文章区域展示都大致相同,刚好也在封装的列表组件里。于是将收集曝光量的逻辑都交由组件内部处理。
组件改造:
- 定义
isObserver
属性,该属性由外部传入的布尔值控制是否收集曝光量 - 监听传入的
list
,为每个元素绑定交叉观察者
以下部分代码省略,只展示主要逻辑:
<block wx:for="{{list}}" wx:key="id">
<view class="artic-item artic-item-{{index}}" data-id="{{item.id}}" data-index="{{index}}">
view>
block>
const app = getApp()
Component({
data: {
currentLen: 0
}
properties: {
list: {
type: Array,
value: []
},
isObserver: {
type: Boolean,
value: false
}
},
observers: {
list(list) {
if (this.data.isObserver === false) {
return
}
if (list.length) {
// currentLen 记录当前列表的长度
// 用于计算监听元素的索引,对已经监听过的元素不再重复监听
let currentLen = this.data.currentLen
for (let i = 0; i let ob = this.createIntersectionObserver({
thresholds: [1]
})
ob.relativeToViewport()
.observe('.artic-item-' + (currentLen + i), res => {
// 获取元素的dataset
let {
id,
index
} = res.dataset
if (res.intersectionRatio === 1) {
// 此处收集曝光量,内部处理逻辑会在下面提及
this.sendExsureId(id)
// 元素出现后取消观察者监听,避免重复触发
ob.disconnect()
}
})
}
}
this.data.currentLen = list.length
}
}
})
![9908ecc5028fc3b617653a458d2a2bcc.gif](https://i-blog.csdnimg.cn/blog_migrate/bdda9e94bc505498de4a3969a3a0b12c.gif)
发现?
![bbbcf1d5c52553fc38f70625bfbe5d78.gif](https://i-blog.csdnimg.cn/blog_migrate/0cbb9b359ffab8636cd3affb28a9fde4.gif)
理想情况应该是切换到第二个分类打印3个文章,但由于组件开始记录第一个分类列表的 currentLen
,在切换到第二个分类时, currentLen
没有被清除,导致循环长度错误。
解决:首先记录列表第一项的 id
,当监听列表变化,用新列表的第一项 id
作与之比较。若不相等,则表示列表被重新赋值,此时将 currentLen
置为0。
Component({
data: {
flagId: 0,
currentLen: 0
}
properties: {
list: {
type: Array,
value: []
},
isObserver: {
type: Boolean,
value: false
}
},
observers: {
list(list) {
if (this.data.isObserver === false) {
return
}
if (list.length) {
// 比较id
if (this.data.flagId != list[0].id) {
this.data.currentLen = 0
}
let currentLen = this.data.currentLen
for (let i = 0; i let ob = this.createIntersectionObserver({
thresholds: [1]
})
ob.relativeToViewport()
.observe('.artic-item-' + (currentLen + i), res => {
let {
id,
index
} = res.dataset
if (res.intersectionRatio === 1) {
this.sendExsureId(id)
ob.disconnect()
}
})
}
}
// 设置列表第一项id
this.data.flagId = list[0] ? list[0].id : 0
this.data.currentLen = list.length
}
}
})
组件优化
因为需要提前监听文章的相交状态,在 list
传入时就开始循环 observe
。现在假设一个场景,在进入页面时,已经为一些文章注册完成回调,但用户并没有看过这些文章就退出页面。那是不是表示这些实例都没有被 disconnect
。
解决:在 observe
时将每一个观察者实例存入数组,当组件销毁时检查数组中是否有观察者实例,如果有,则调用这些实例的 disconnect
。
Component({
data: {
flagId: 0,
currentLen: 0,
obItems: [] // 存放实例的数组
},
properties: {
list: {
type: Array,
value: []
},
isObserver: {
type: Boolean,
value: false
}
},
observers: {
list(list) {
if (this.data.isObserver === false) {
return
}
if (list.length) {
if (this.data.flagId != list[0].id) {
this.data.currentLen = 0
// 取消实例的监听
this.removeObItems()
}
let currentLen = this.data.currentLen
for (let i = 0; i let ob = this.createIntersectionObserver({
thresholds: [1]
})
ob.relativeToViewport().observe('.artic-item-' + (currentLen + i), res => {
let {
index,
id
} = res.dataset
if (res.intersectionRatio === 1) {
this.sendExsureId(id)
ob.disconnect()
// 取消监听后 将实例移出数组
this.data.obItems.shift()
}
})
// 将实例存入数组
this.data.obItems.push(ob)
}
} else {
// 取消实例的监听
this.removeObItems()
}
this.data.flagId = list[0] ? list[0].id : 0
this.data.currentLen = list.length
}
},
lifetimes: {
detached() {
// 组件销毁时 取消实例的监听
this.removeObItems()
}
},
methods: {
removeObItems() {
if (this.data.obItems.length) {
this.data.obItems.forEach(ob => {
ob.disconnect()
})
}
}
}
})
收集处理
现在组件能够收集到曝光文章的ID,剩下的就是往后台发送数据。那么问题来了,难道文章曝光一次就发起一次请求吗?如果不怕和后端同事干架的话,你可以这么做。要知道多次发起请求,服务器?会很大。用户量比较大后,对服务器能够承受的并发量会有很大的考验。所以正确的做法应该是,把收集到的ID缓存起来,在达到一定数量的时候一起发送过去。
接下来对收集的数据做些处理:
// 这个上面收集曝光量的函数
sendExsureId(id) {
if (typeof app.globalData.exposureIds === 'undefined') {
// exposureIds 是定义在全局用于存放曝光文章 ID 的数组
app.globalData.exposureIds = []
}
app.globalData.exposureIds.push(id)
// 当数组到达 50 个,开始上报数据
if (app.globalData.exposureIds.length >= 50) {
wx.$api.recordExposure({
// 因为 ID 比较多,我和后端约定好使用逗号分隔
ids: app.globalData.exposureIds.join(',')
})
// 上报后清空数组
app.globalData.exposureIds = []
}
}
看起来好像实现到这里就大功告成,但是我们还要考虑一种情况。假如用户只看了 40 个就退出小程序,而上报条件是达到 50 个才会发送数据,那么这部分有用的数据就会被丢失。因为小程序没有回调能够监听到小程序被销毁,这里只能使用小程序的 onHide
函数来做些事情。当小程序进入后台时 onHide
函数就会被执行,此时可以在函数里上报数据。
App({
onHide() {
if (this.globalData.exposureIds.length) {
wx.$api.recordExposure({
ids: this.globalData.exposureIds.join(',')
})
this.globalData.exposureIds = []
}
}
})
写在最后
说实话,在埋点这方面的知识不算很熟悉,业务场景也比较简单。因为没有大佬指导,也是看着需求往这方面去做,有哪里错误或遗漏请指出。如果你有更好的方案或经验,欢迎评论区交流?~