前言
为什么要做这个监测用户停留的呢?原因很简单,如果我们要分析这个页面对我们的产品有没有价格,那么用户浏览的时长是一个很关键的点,如果每个用户平均每天在这个页面停留两个小时以上,那么我们会觉得这个页面的价值很高;如果一个页面一个月也没几个用户去浏览,那我们就会有疑问,这个页面对我们的产品还有价值吗?我们的产品后续还要保留它的?这些需求都是可以让我们考虑是否要去获取用户停留时长这个功能的。
针对哪些应用?
- 多页面应用
- 单页面应用(本文以vue为例子)
如何去获取用户停留的时长?
在监测功能的时候,我们首要考虑的就是,我们的监测代码不能影响我们现在的业务代码,和以后的业务代码。
多页面应用
在多页面应用,要获取用户的停留时间还是挺简单的,看看下面几个Api
- onload[1] (页面加载完后)
- onbeforeunload[2] (页面卸载前,也就是点击叉的时候)
- onpageshow[3] (页面显示的时候)
- onpagehide[4] (页面隐藏的时候)
经过自己的一点小测试,发现无论在关闭的时候(也就是点击叉),还是隐藏的时候(也就是点击左右箭头)都会触发 onpagehide;也无论是在首次加载,还是刷新的时候,都会触发onpageshow,但是刷新的时候会先触发onpagehide,在触发onpageshow。其他两个API也是类似,最后,个人选择了用onpageshow和onpagehide这个两个API获取用户停留的时长,你也可以用其他两个API做。只要在onpageshow初始时间值,在onpagehide的时间求出差值,然后上传到后台就行。
let stopTime window.onpageshow = ()=>{ stopTime = new Date().getTime() } window.onpagehide = ()=>{ stopTime = new Date().getTime() - stopTime let record = localS('data') let data = record && JSON.parse(record) || [] localS('data',JSON.stringify([...data,{user:new Date().getTime(),pa}])) }
完整代码就那么点,这里我只是测试,就把模拟的用户id,真实的路径,真实的停留时长存储了在localStorage,在项目中可以传到后台,然后通过分析,再可视化展示出来。
上面效果图(录得GIF就那AV画质,没钱开VIP,将就看吧,有点AV感觉也好,哈哈),第一次点击的是叉,点击叉那也可以理解为离开了页面,第二次a链接跳转,第三次刷新,刷新也可以理解为一次离开吧,一共存了三条数据,有兴趣自己试试更好理解。
单页面应用
单页面应用可能,会复杂一点,但是也复杂不到哪里去。
单页面应用的路由跳转,都是基于 H5的History API(browserHistory) 和 Hash(hashHistory)实现的。
browserHistory
单页面的browserHistory路由是基于H5的History API实现的,我们只要监听popstate就可以知道,点击前进后退按钮改变的url变化,url发生变化,我们就能统计用户在该页面待了多长时间,代码如下
let timeStr window.addEventListener('onload',(e)=>{ timeStr = new Date().getTime() }) window.addEventListener('popstate',()=>{ let t = new Date().getTime() - timeStr timeStr = new Date().getTime() con('待了时长:'+ t) })
但是,pushState和replaceState(也就是,点击router-view,$rou,$rou,window.,window.不会触发,可以自行试试)不会触发popstate,那我们就统计不了用户待在该页面的时长的了;可是解决方法还是有的,只需要重写pushState和replaceState,然后监听两个自定义事件就行,看下面代码
// 对原函数做一个拓展 let rewriteHis = function(type){ let origin = window.history[type] // 先将原函数存放起来 return function(){ // 当window.history[type]函数被执行时,这个函数就会被执行 let rs = origin.apply(this, arguments) // 执行原函数 let e = new Even()) // 定义一个自定义事件 e.arguments = arguments // 把默认参数,绑定到自定义事件上,new Event返回的结果,自身上是没有arguments的 window.dispatchEvent(e) // 触发自定义事件,把载荷传给自定义事件 return rs } } window. = rewriteHis('pushState') // 覆盖原来的pushState方法 window. = rewriteHis('replaceState') // 覆盖原来的replaceState方法 // 监听自定义事件, pushstate事件是在rewriteHis时注册的,不是原生事件 // 当点击router-link 或者 window. 或者 this.$rou 时都会被该事件监听到 window.addEventListener('pushstate',()=>{}) // 监听自定义事件, replacestate事件是在rewriteHis时注册的,不是原生事件 // 当点击window. 或者 this.$rou 时都会被该事件监听到 window.addEventListener('replacestate',()=>{})
rewriteHis函数,这个函数主要是对原函数做了一个拓展,上面代码的注释应该说了很清楚了。
browserHistory路由变化监听完整代码
let timeStr let rewriteHis = function(type){ let origin = window.history[type] return function(){ let rs = origin.apply(this, arguments) let e = new Even()) e.arguments = arguments window.dispatchEvent(e) return rs } } window. = rewriteHis('pushState') window. = rewriteHis('replaceState') window.addEventListener('onload',(e)=>{ timeStr = new Date().getTime() }) window.addEventListener('popstate',()=>{ let t = new Date().getTime() - timeStr timeStr = new Date().getTime() con('待了时长popstate:'+ t) }) window.addEventListener('pushstate',()=>{ let t = new Date().getTime() - timeStr timeStr = new Date().getTime() con('待了时长pushstate:'+ t) }) window.addEventListener('replacestate',()=>{ let t = new Date().getTime() - timeStr timeStr = new Date().getTime() con('待了时长replacestate:'+ t) })
这里的演示就不做任何存储了,喜欢的话,自己做一下
hashHistory
hashHistory就简单得不行了,直接监听hashchange就行
window.addEventListener('hashchange',()=>{ let t = new Date().getTime() - timeStr timeStr = new Date().getTime() con('待了时长:'+ t) })
到这里为止,单页面应用,多页面应用怎么去获取用户待在该页面的时长就说完了。也不是很理解,挺简单的。
后续
你以为这就完事了吧,还有一个奇怪的问题。
const router = new VueRouter({ mode:'hash', routes:[...] })
我在测试hash路由切换的时候,看会不会触发window.addEventListener('hashchange',()=>{}),奇怪的事情发生了,它没有触发,却触发了自定义window.addEventListener('replacestate',()=>{}和window.addEventListener('pushstate',()=>{})等History API,究竟是为什么?我明明把它设置为hash路由了,为什么还会触发History API?带着这个疑问,我忍不住的去看了vue-router的源码,最后,解开了自己的疑问,看下面:
在vue-router的hash路由实现文件
有这么一段代码,在 supportsPushState为false时,才会走else逻辑,else的逻辑才会触发window.addEventListener('hashchange',()=>{}),那它为什么不走?supportsPushState又是什么?pushState,replaceState又是怎么实现的?为什么它会触发自定义事件?
supportsPushState
来看看supportsPushState是什么
上面是supportsPushState的逻辑,看到这逻辑,是不是瞬间就明白了为什么不走else逻辑的hash语句了。当满足这些条件才会走else语句('Android 2.') !== \-1 || ua.indexOf('Android 4.0') !== \-1) && ua.indexOf('Mobile Safari') !== \-1 && ua.indexOf('Chrome') === \-1 && ua.indexOf('Windows Phone') === \-1,否则其他都是走基于History API实现的Hash-router。
pushState , replaceState
再来看看这两个api的实现
原来它们都是调用了 History API实现,这就解开了为什么它会触发自定义事件的原因了。
看源码指引
如果你不相信,我比比的话,可以自行看看源码。