最近接到产品的需求,需要在小程序原生以及webview侧实现首页、列表页、订单填写页以及订单详情页的停留时间埋点。一开始觉得挺简单,微信原生页有onShow
、onHide
,vue页面有mounted
、beforeDestory
等生命周期钩子,只需要在离开的时候计算差值,然后上报埋点就行了。但后来发现并不是这样。
首先,微信小程序原生页面(以首页为例)
// index.js
let pageStartTime = null
let pageEndTime = null
Page({
data: {
openId: '',
unionId: ''
},
onLoad(options){
},
onShow() {
pageStartTime = Date.now()
},
onHide() {
pageEndTime = Date.now()
let stayTime = Math.round((pageEndTime - pageStartTime) / 1000)
wx.$setEvent('首页', `停留时长:${stayTime}秒`)
}
})
基本上正常的页面跳转,统计首页的停留时间没什么问题。但是还有一个要考虑的点,就是onShow
和onHide
不仅在页面切换的时候会触发,小程序切换到后台和回到前台,也会触发。这个要看产品侧需要怎么统计了,如果切换后台一会又回到前台,页面停留时间继续保留的话,那单纯靠onShow
和onHide
就不行了。
这个时候可以用app.js里面的onShow
和onHide
,因为它们不会在页面切换的时候触发,但在小程序进入前台后台的时候会触发。
解决方案其实可以在globalData里面设置全局变量,然后app.js的onShow
和onHide
触发时更改变量,页面的onShow
和onHide
触发时根据全局变量去做pageStartTime时间初始化处理。
// app.js
onShow(){
if(this.globalData.initFirst){
this.globalData.initFirst = 0
}else{
this.globalData.onShow = 1
}
},
onHide(){
this.globalData.onHide = 1
}
// index.js
let pageStartTime=null,pageEndTime=null,app=getApp()
page({
onShow(){
setTimeout(()=>{
if(app.globalData.onShow){
app.globalData.onShow = 0
}else{
pageStartTime = Date.now()
}
},100)
},
onHide(){
setTimeout(()=>{
if(app.globalData.onHide){
app.gloablData.onHide = 0
}else{
pageEndTime = Date.now()
let stayTime = Math.round((pageEndTime - pageStartTime) / 1000)
wx.$setEvent('首页', `停留时长:${stayTime}秒`)
}
},100)
}
})
vue页面
<template>
<div class="main">
123
</div>
</template>
<script>
let pageStartTime = null
let pageEndTime = null
export default{
name: 'index',
data(){
return {}
},
mounted(){
pageStartTime = Date.now()
},
beforeDestroy(){
pageEndTime = Date.now()
let stayTime = Math.round((pageEndTime - pageStartTime) / 1000)
window.$setEvent('首页', `停留时长:${stayTime}秒`)
}
}
</script>
这样埋点的话会存在一个问题,因为beforeDestroy钩子在同级路由跳转的时候会触发,但是如果是当前页面跳转子路由页面的话,就不会触发这个钩子函数。比如这种:
从home页跳转children下的子路由页面时,并不会触发beforeDestroy钩子函数。 这时候如果子路由页面只是当前页面的一个局部ui,那没问题,但是如果是页面覆盖,比如右侧划入等等,就等同于跳转到其他页面了,这个时候停留时间统计就出错了。
有几种解决办法:
最简单暴力的就是在跳转子路由的方法里计算停留时间并上报,并初始化时间,但是这有一个弊端,因为一个页面有多个组件,每个组件里面有单独的子路由跳转,需要每个跳转方法里面添加埋点,另外后续只要在当前页面添加新的子路由跳转,都要做兼容处理,那简直难搞,直接pass。
第二个在全局路由钩子(router.beforeEach、 router.afterEach)里面添加埋点拦截,因为不管是同级路由跳转还是跳转子路由,都会触发路由钩子,这时候只需要在这里记录页面停留时间并上报就行了。
笔者这里还是会在页面mounted时定义页面开始时间,而不是直接在路由钩子里面定义,因为路由钩子优先mounted钩子触发,但是正常页面展现是mounted钩子,页面简单还好,时间不会相差太多,但是当页面复杂,组件很多,渲染慢的时候,这时候在路由钩子里面定义pageStartTime就会导致页面还未渲染出来,已经开始记录时间了。
// index.vue
<template>
<div class="main">
123
</div>
</template>
let pageEndTime = null
<script>
export default{
name: 'index',
data(){
return {}
},
mounted(){
window.$pageStartTime = Date.now()
},
beforeDestroy(){
pageEndTime = Date.now()
let stayTime = Math.round((pageEndTime - window.$pageStartTime) / 1000)
window.$setEvent('首页', `停留时长:${stayTime}秒`)
}
}
</script>
然后在全局路由钩子里面做停留时间处理。
// 获取index页面children子路由列表
let indexChildren = routers.find(item => {
return item.name === 'home'
}).children.map(item => {
return item.name
})
router.beforeEach((to, from, next)=>{
// 从子路由返回首页,初始化开始时间
if (to.name === 'home' && indexChildren.includes(from.name)) {
window.$pageStartTime = Date.now()
}
if (to.name && indexChildren.includes(to.name)) {
let stayTime = Math.round((Date.now() - window.$pageStartTime) / 1000)
window.$setEvent('首页', `停留时长:${stayTime}秒`)
}
next()
})
目前笔者根据这样来实现H5页面停留时间的埋点。
当然还是有一定的瑕疵,就是必须定义全局变量。当埋点页面多的话,就要定义多个全局变量。
后面考虑的方案是,如果以后每个页面都添加停留时间埋点的话,可以将mounted里面时间的初始化逻辑加入到mixins里面,只需一个全局变量。然后在路由钩子里面计算埋点时间就行。
不知道还有没有小伙伴有更好的处理逻辑,欢迎指教。
---------------------------------- 用mixins改造后 -------------------------------------
首先,先添加mixins文件
//commonMix/stayTime.js
import { mapState } from 'vuex'
export default {
data() {
return {}
},
computed: {
...mapState(['productType'])
},
mounted() {
window.$pageStartTime = Date.now()
},
beforeDestroy() {
let stayTime = Math.round((Date.now() - window.$pageStartTime) / 1000)
// 开始埋点
window.$tcevt('首页', this.productType, 'tlsc.sy', `停留时长:${stayTime}秒`)
}
}
然后,页面中引入
// index.vue
import timeMix from '@/commonMix/stayTime'
<template>
<div class="main">
123
</div>
</template>
let pageEndTime = null
<script>
export default{
mixins: [timeMix],
name: 'index',
data(){
return {}
},
mounted(){
window.$pageStartTime = Date.now()
},
beforeDestroy(){
pageEndTime = Date.now()
let stayTime = Math.round((pageEndTime - window.$pageStartTime) / 1000)
window.$setEvent('首页', `停留时长:${stayTime}秒`)
}
}
</script>
最后在router.beforeEach钩子函数中添加页面拦截
// router/index.js
let stayTimePageList = [
{ id: 'home', label: 'tlsc.sy', name: '首页' },
{ id: 'list', label: 'tlsc.lb', name: '列表页' },
{ id: 'book', label: 'tlsc.tx', name: '填写页' },
{ id: 'detail', label: 'tlsc.xq', name: '详情页' }
]
router.beforeEach((to, from, next) => {
for (let i = 0; i < stayTimePageList.length; i++) {
let pageItem = stayTimePageList[i]
if (to.name === pageItem.id) {
let childrenList = routers.find(item => {
return item.name === pageItem.id
}).children.map(item => {
return item.name
})
if (childrenList.includes(from.name)) {
window.$pageStartTime = Date.now()
}
}
if (from.name === pageItem.id) {
let childrenList = routers.find(item => {
return item.name === pageItem.id
}).children.map(item => {
return item.name
})
if (childrenList.includes(from.name)) {
window.$pageStartTime = Date.now()
}
if (to.name && childrenList.includes(to.name)) {
let stayTime = Math.round((Date.now() - window.$pageStartTime) / 1000)
window.$tcevt(pageItem.name, store.state.productType, pageItem.label, `停留时长:${stayTime}秒`)
}
}
}
//.....后面是业务代码
})