关于微信小程序原生页与H5页面(vue侧)停留时间的埋点实现探讨

lxf2023-04-25 18:34:01

最近接到产品的需求,需要在小程序原生以及webview侧实现首页、列表页、订单填写页以及订单详情页的停留时间埋点。一开始觉得挺简单,微信原生页有onShowonHide,vue页面mountedbeforeDestory等生命周期钩子,只需要在离开的时候计算差值,然后上报埋点就行了。但后来发现并不是这样。

首先,微信小程序原生页面(以首页为例)
// 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}秒`)
    }
})

基本上正常的页面跳转,统计首页的停留时间没什么问题。但是还有一个要考虑的点,就是onShowonHide不仅在页面切换的时候会触发,小程序切换到后台和回到前台,也会触发。这个要看产品侧需要怎么统计了,如果切换后台一会又回到前台,页面停留时间继续保留的话,那单纯靠onShowonHide就不行了。

这个时候可以用app.js里面的onShowonHide,因为它们不会在页面切换的时候触发,但在小程序进入前台后台的时候会触发。

解决方案其实可以在globalData里面设置全局变量,然后app.js的onShowonHide触发时更改变量,页面的onShowonHide触发时根据全局变量去做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钩子在同级路由跳转的时候会触发,但是如果是当前页面跳转子路由页面的话,就不会触发这个钩子函数。比如这种:

关于微信小程序原生页与H5页面(vue侧)停留时间的埋点实现探讨 从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}秒`)
            }
        }
    }
    
    //.....后面是业务代码
})