微信小程序曝光埋点-createIntersectionObserver小记

2,853 阅读3分钟

微信小程序环境,对某个列表的埋点曝光需求为下
1.item在手机可视区域内完全展示
2.item在可视区域内停留不少于1s
3.上下重复滑动不重复计入曝光
4.重新刷新页面会再次记录打桩

根据调查发现createIntersectionObserver比较符合,搞起。 官方文档:developers.weixin.qq.com/miniprogram… 因页面包含组件故使用api: this.createIntersectionObserver([options]) 其中options包含三个参数thresholds、initialRatio、observeAll
thresholds:触发观察阈值的数值,Array类型,值为0-1。比如[0,1]就代表刚进入,完全进入,刚离开,完全离开时会触发观察。这里经过我反复思考测试,发现[1]是最符合使用场景的,就是完全进入以及刚离开的情况触发观察。
initialRatio:初始的相交比例,如果调用时检测到的相交比例与这个值不相等且达到阈值,则会触发一次监听器的回调函数。Number类型。这个参数比较重要,比如一开始就有在屏幕内完全显示的item就需要设置为0,不然在没进行滚动条操作时无法观察到当前需要观察的对象
observeAll:是否同时观测多个目标节,Boolean类型。因为我们是观察列表,这里填true。

直接看代码,解释在代码里,应该比较好理解了:
第一版:

<template>
    <div>
        <div v-for="(item, index) in lists" :key="index" class="item" :data-index="index">
            {{ item.name }}
        </div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            lists: [{ name: 'a1' }, { name: 'a2' }, { name: 'a3' }]
        }
    },
    methods: {
        push(item) {
            console.log(`上报数据:${item.name}`)
            item.hadPushData = true  //打上标志
        }
    },
    onReady() {
        console.clear()
        const TIME = 3000  //最短的曝光时间
        this.observe = this.createIntersectionObserver({ thresholds: [1], initialRatio: 0, observeAll: true })
        this.observe.relativeToViewport().observe('.item', (res) => {
            let item = this.lists[res.dataset.index]  //这里注意,在html那需要把index传进来,比如:data-index="index",然后才能拿到对应的item
            if (item.hadPushData) return //上报过的跳过
            let isAllEnter = res.intersectionRatio == 1 //为1则为整个item彻底曝光在屏幕内了
            item[isAllEnter ? 'timeEnterAll' : 'timeLeave'] = res.time //依次打上两个状态的时间戳
            if (item.timeLeave) {
                // 有了timeLeave说明离开了,此时判断曝光时间
                let stamp = item.timeLeave - item.timeEnterAll
                // 曝光时间够长 上报
                if (stamp >= TIME) this.push(item)
            }
        })
    }
}
</script>
<style lang="scss" scoped>
.item {
    height: 1000rpx;
}
</style>

最开始时想的思路是记录进入的时间timeEnterAll,以及离开的时间,如果二者相差时间大于1s则上报。这里的关键是options参数是{thresholds: [1], initialRatio: 0, observeAll: true}。\

后来发现有一种情况是只进入了,没有离开,也就是一直逗留在当前页面,没有timeLeave,此时增加采用了一个定时器来判断,1s后还没有timeLeave则也可以上报。

<template>
    <div>
        <div v-for="(item, index) in lists" :key="index" class="item" :data-index="index">
            {{ item.name }}
        </div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            lists: [{ name: 'a1' }, { name: 'a2' }, { name: 'a3' }]
        }
    },
    methods: {
        push(item) {
            console.log(`上报数据:${item.name}`)
            item.hadPushData = true //打上标志
        }
    },
    onReady() {
        console.clear()
        const TIME = 3000 //最短的曝光时间
        this.observe = this.createIntersectionObserver({ thresholds: [1], initialRatio: 0, observeAll: true })
        this.observe.relativeToViewport().observe('.item', (res) => {
            let item = this.lists[res.dataset.index] //这里注意,在html那需要把index传进来,比如:data-index="index",然后才能拿到对应的item
            if (item.hadPushData) return //上报过的跳过
            let isAllEnter = res.intersectionRatio == 1 //为1则为整个item彻底曝光在屏幕内了 ①
            item[isAllEnter ? 'timeEnterAll' : 'timeLeave'] = res.time //依次打上两个状态的时间戳
            if (item.timeLeave) {
                // 有了timeLeave说明离开了,此时判断曝光时间
                let stamp = item.timeLeave - item.timeEnterAll
                // 曝光时间够长 上报
                if (stamp >= TIME) this.push(item)
            } else {
                // 进入没离开流程
                item.timer = setTimeout(() => {
                    // 曝光时间后还没有timeLeave说明还在当前屏幕没离开
                    if (!item.timeLeave) this.push(item)
                }, TIME)
            }
        })
    }
}
</script>
<style lang="scss" scoped>
.item {
    height: 1000rpx;
}
</style>

然后还要考虑快速滚动进入离开的情况后,又拖回来了。关键代码来了

  onReady() {
        console.clear()
        const TIME = 3000 //最短的曝光时间
        this.observe = this.createIntersectionObserver({ thresholds: [1], initialRatio: 0, observeAll: true })
        this.observe.relativeToViewport().observe('.item', (res) => {
            let item = this.lists[res.dataset.index] //这里注意,在html那需要把index传进来,比如:data-index="index",然后才能拿到对应的item
            if (item.hadPushData) return //上报过的跳过
            let isAllEnter = res.intersectionRatio == 1 //为1则为整个item彻底曝光在屏幕内了
            if (isAllEnter && item.timeLeave) delete item.timeLeave //往回啦时清空离开时间 --!重点
            item[isAllEnter ? 'timeEnterAll' : 'timeLeave'] = res.time //依次打上两个状态的时间戳
            if (item.timeLeave) {
                // 有了timeLeave说明离开了,此时判断曝光时间
                let stamp = item.timeLeave - item.timeEnterAll
                // 曝光时间够长 上报
                if (stamp >= TIME) {
                    push(item)
                } else {
                    // 曝光时间太短
                    if (item.timer) clearTimeout(item.timer)
                }
            } else {
                // 进入没离开流程
                item.timer = setTimeout(() => {
                    // 曝光时间后还没有timeLeave说明还在当前屏幕没离开
                    if (!item.timeLeave) this.push(item)
                }, TIME)
            }
        })
    }

if (isAllEnter && item.timeLeave) delete item.timeLeave

意思是item这次是完全展示,又记录到上一次离开的时间,说明是符合快速进入离开的情况。此时删除掉timeLeave,回归到初始状态,然后就能简单的记录曝光时间了。
此时已经满足所有需求。 然后还要考虑到分页加载、页面销毁的情况,记得补充

if (this.observe) this.observe.disconnect()

后面遇到一种情况,如图

企业微信截图_16287629129285.png 底部有个固定fix按钮,遮挡住了列表,这种情况并不符合上报的场景,需要简单如下调整一下,bottom的值为fix固定按钮的高度,单位px,这样就做到了真正的视野内才上报。

this.observe.relativeToViewport({bottom:-112})

最后还剩下虚拟列表、请求缓存池的情况,下回补充。
以上纯属自己考虑的方案,如有雷同算我抄你。