前提
最近项目有需求,需要对一个小程序以及官网项目就行数据埋点上报,但是又不让使用第三方的SDK,要自己封装,哎,难搞,没办法,就各种抠脑壳查资料,在这做个笔记,总结一下
什么是数据埋点,有什么用
个人理解就是对网站,项目进行业务数据,用户行为数据及其他实际需要的数据进行采集上报,是了解用户行为,分析用户行为,提高用户体验的一种方式,通过这些采集来的数据就可以进行分析,知道用户来源,访问量,点击量,停留时长等数据,将用户使用产品的行为进行可视化,对产品进行优化调整,
常见的解决方案有三种,代码埋点、可视化埋点、和无埋点(全埋点)三种。这三种大体上字面意思也能懂,具体介绍我在这就偷懒不写了,自行百度吧
实现方式
我在项目中 主要采用的是全埋点和代码埋点相结合的方式,因为要采集一部分业务数据,全埋点满足不了,只能结合代码埋点来进行了
全埋点实现过程
全埋点主要是采集一些基本信息,比如浏览器版本号,页面路径,屏幕信息,停留时长,点击事件等数据。而且重要的一点是监测代码不能影响我们现在的业务代码,和以后的业务代码。要解耦。否则难以维护
基础信息采集
首先封装一个对象,包括基础数据,基础配置,数据上报方法,初始化基本信息采集方法,数据上报的方法有很多种,此处是借助访问图片资源的方法来,可以避免跨域,防止阻塞主流程,当然也可以直接使用ajax上报,只要上报的时候如果接口报错异常什么的不要影响到主业务流程即可,下面代码示例中,只是简单的demo,如需其他数据,就自行添加即可
var collect = {
params: {}, // 传递或上报的参数
data: {
serverUrl: "", //上报接口
EVENT_PREFIX: "longan-", //全局事件过滤节点前缀
},
// 设置基本参数配置
setData: function (data) {
this.data = Object.assign({}, this.data, data);
},
// 初始化
init: function () {
//Document对象数据
if (document) {
this.params.domain = document.domain || ""; //获取域名
this.params.url = document.URL || ""; //当前Url地址
this.params.title = document.title || "";
this.params.referrer = document.referrer || ""; //上一跳路径
}
//Window对象数据
if (window && window.screen) {
this.params.sh = window.screen.height || 0; //获取显示屏信息
this.params.sw = window.screen.width || 0;
this.params.cd = window.screen.colorDepth || 0;
this.params.origin = window.location.origin;
this.params.pathname = window.location.pathname;
}
//navigator对象数据
if (navigator) {
this.params.lang = navigator.language || ""; //获取所用语言种类
}
},
// 上报数据
sendRequest: (config) => {
config.route = window.location.origin + window.location.pathname;
config.createTime = (new Date().getTime() / 1000).toFixed(0);
// 直接上报
const image = new Image();
image.src = data.serverUrl + "?" + parseParams(params);
},
};
监听全局的点击事件
大部分埋点都会有对点击事件的采集,这块采集逻辑不能写入业务代码中,避免污染业务逻辑代码,后期难以维护,一般是需要监听特定的功能区按钮,banner等的点击事件,不需要对全局所有节点的点击做采集,所以要做好过滤,通过data-***=’—'的方式传递参数,定义加上事件前缀的id,进行过滤
// 监听全局事件埋点,此处还是属于collect这个对象的一个方法
listenTriggerEvent: function (config) {
const EVENT_PREFIX = this.data.EVENT_PREFIX;//全局事件过滤节点前缀
document.addEventListener("click", (e) => {
var dataset = e.target.dataset
// 如果目标节点存在事件前缀 则继续
if (e.target.id && e.target.id.startsWith(EVENT_PREFIX)) {
// ...... 自己搞一些事情.......下面是个示例
//const eventName = e.target.id.split(EVENT_PREFIX)[1];
//const eventContent = e.target.innerHTML || "";
//const payload = Object.assign(config,dataset,{
// type: "event-click",
// client: navigator.userAgent,
// content: eventContent,
// name: eventName,
// })
//console.log(payload);
//上传埋点信息fn
// sendRequest(payload);
}
});
},
采集页面停留时长
此处分两种情况单页面应用和多页面应用
- 多页面应用 (传统的原生写法。没有用spa框架开发的)
在多页面应用中获取停留时长挺简单的,就是在进入页面或页面显示的时候记录下时间,然后离开页面的时候记录下时间,可以前端算好时间差,也可以直接传给后台由后台计算时间差。主要用到下面几个api-
onload (页面加载完后)
-
onbeforeunload (页面卸载前,也就是点击叉的时候)
-
onpageshow (页面显示的时候)
-
onpagehide (页面隐藏的时候)
以上随意取一组就可以了。下面是代码示例,getCurrentTime()是个取当前时间戳的方法,看你们项目需求,自定义返回时间就可以了
-
listenHistory: function (params) {
window.onload = function () {
params.createTime = getCurrentTime();
params.type = '进入页面'
console.log('onload',params);
};
window.onbeforeunload = function () {
params.leaveTime = getCurrentTime();
params.type = '离开页面'
params.stayLength= params.leaveTime - params.createTime
localStorage.setItem('onbeforeunload',JSON.stringify(params))
};
},
- 单页面应用
单页面应用复杂些,此处以Vue举例,Vue的路由模式有两种,一种是History,另一种是Hash模式
- History模式是基于H5的History API实现的,我们只要监听popstate就可以知道,点击前进后退按钮改变的url变化,监听到url发生变化,我们就能统计用户在该页面待了多长时间
window.addEventListener('onload',(e)=>{
params.createTime = getCurrentTime()
})
window.addEventListener('popstate',()=>{
params.leaveTime = getCurrentTime()
params.stayLength= params.leaveTime - params.createTime
console.log('待了时长:'+ t)
})
但是监听popstate会存在漏洞,看下图
所以还需要重写pushState和replaceState,然后监听两个自定义事件就行,
let resetHistoryFun= function(type){
// 将原先的方法复制出来
let origin = window.history[type]
// 当window.history[type]函数被执行时,这个return出来的函数就会被执行
return function(){
// 执行原先的方法
let rs = origin.apply(this, arguments)
// 然后自定义事件
let e = new Event(type.toLocaleLowerCase())
// 将原先函数的参数绑定到自定义的事件上去,原先的是没有的
e.arguments = arguments
// 然后用window.dispatchEvent()主动触发
window.dispatchEvent(e)
return e
}
}
--------------------------------
// 更新 20210528
// 最近新了解到哟个新的api 用于自定义事件,但是这个可以传递参数
let e = new CustomEvent(事件名, 参数)
---------------------------------------------
window.history.pushState = rewriteHis('pushState') // 覆盖原来的pushState方法
window.history.replaceState = rewriteHis('replaceState') // 覆盖原来的replaceState方法
// 监听自定义事件, pushstate事件是在rewriteHis时注册的,不是原生事件
// 当点击router-link 或者 window.history.pushState 或者 this.$router.push 时都会被该事件监听到
window.addEventListener('pushstate',()=>{})
// 监听自定义事件, replacestate事件是在rewriteHis时注册的,不是原生事件
// 当点击window.history.replaceState 或者 this.$router.replace 时都会被该事件监听到
window.addEventListener('replacestate',()=>{})
下面是统计时长的具体一点的方法
window.addEventListener('load',(event)=>{
timeStr = new Date().getTime()
})
window.addEventListener('popstate',(event)=>{
let t = new Date().getTime() - timeStr
timeStr = new Date().getTime()
console.log('待了时长popstate:'+ t)
})
window.addEventListener('pushstate',(event)=>{
let t = new Date().getTime() - timeStr
timeStr = new Date().getTime()
console.log('待了时长pushstate:'+ t)
})
window.addEventListener('replacestate',(event)=>{
let t = new Date().getTime() - timeStr
timeStr = new Date().getTime()
console.log('待了时长replacestate:'+ t)
})
- hash模式的话是直接监听
hashchange
事件就可以了
window.addEventListener('hashchange',()=>{
let t = new Date().getTime() - timeStr
timeStr = new Date().getTime()
console.log('待了时长:'+ t)
})
但是你会发现 ,咦,我的路由模式是hash模式 怎么不触发hashchange事件呢,主要原因在vue-router源码中有体现,大部分的浏览器中,hash模式的实现还是基于History API实现的Hash-router。具体原因就看源码,或者看这里,这个文章中后续说的很详细
以上就是大概自己封装埋点方法的内容了,可以把上述功能代码用闭包给包裹起来,保存私有变量,避免污染全局,只暴漏出来一些方法,或可以把这个js文件使用rollup
打包成各种版本的sdk文件,与业务代码完全分离,供多个项目使用。如果有更好的实现方式,欢迎交流。