theme: channing-cyan
前言
最近埋点业务接触的不少,于是乎想整理一篇相关的文章出来,分享给大家,也便于自己阅读。
由于是使用 vue2.x 实现的业务,所以埋点是基于vue2.x来的(什么技术栈不重要逻辑是一样的)。
如果是自己想玩一下,可以使用百度的埋点统计(npm包 vue-ba): 传送门
埋点
如果是内部自己的埋点统计,需要理清一下埋点触发的几种时机:
- ready: 进入指定页面时触发
- click: 点击指定元素时触发
- view: 指定区域眼球曝光时触发
- unload: 离开指定页面时触发
埋点
进入指定页面触发埋点是很常见的埋点行为,最简单的方式就是在路由守卫调取埋点接口即可。但是为了不在每个页面的路由守卫重复书写,我们可以统一抽取封装埋点行为。
这里用到两个比较常见的工具库:dayjs、underscore(不用也可以,看个人)
代码语言:javascript复制import _ from 'underscore'
import dayjs from '@app/js/lib/dayjs'
import tracker from './tracker.js'
export const trackData = (data) => {
const params = {
head: {
token: 'xxx', // token
sendTime: dayjs().valueOf(), // 发送时间
},
serviceDatas: [{
eventId: data.id, // 事件ID
occurTime: dayjs().valueOf(), // 事件触发时间
serviceParam: data.data, // 事件数据
}]
}
tracker(params)
}
可以看到上述方法,简单的处理了一下数据并调取接口即可,可以适用大部分埋点如 ready、click、unload等。但是 view 的话怎么触发呢?因为涉及到元素位置等一系列问题,所以我们再封装一个方法来解决这个问题,一劳永逸。
代码语言:javascript复制import _ from 'underscore'
import Vue from 'vue'
// 判断当前元素是否在可视区域
const isInView = (el) => {
var rect = el.getBoundingClientRect()
var elemTop = rect.top
var elemBottom = rect.bottom
// 元素全部出现在视窗
var isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
return isVisible;
}
// 生成随机函数属性名
const createFunName = () => {
return `track_f_${(Math.random() * 1000000 '').split('.')[0]}`
}
// 已绑定的事件处理函数集合
const FunCollection = {}
// 进入页面处理函数
const readyFun = (el, binding) => {
const occurTime = el.dataset.enterTime
const params = getParams(el, binding, occurTime)
tracker(params)
}
// 点击处理函数
const clickFun = (el, binding) => {
const occurTime = dayjs().valueOf()
const params = getParams(el, binding, occurTime)
tracker(params)
el.removeEventListener('click', FunCollection[el.dataset.clickFun])
}
// 眼球曝光处理函数
const viewFun = _.throttle((el, binding) => {
if (isInView(el)) {
const occurTime = dayjs().valueOf()
const params = getParams(el, binding, occurTime)
tracker(params)
window.removeEventListener('scroll', FunCollection[el.dataset.viewFun])
}
}, 100)
// 离开页面处理函数
const unloadFun = (el, binding) => {
const occurTime = el.dataset.enterTime
el.dataset.leaveTime = dayjs().valueOf()
const params = getParams(el, binding, occurTime)
tracker(params)
window.removeEventListener('beforeunload', FunCollection[el.dataset.unloadFun])
}
// 埋点事件逻辑
const track = (el, binding, forceRun = false) => {
const type = binding.value.act
if (binding.value.act === 'ready') {
readyFun(el, binding)
}
if (type === 'click') {
const cf = el.dataset.clickFun
if (cf && FunCollection[cf]) {
el.removeEventListener('click', FunCollection[cf])
delete FunCollection[cf]
}
const fs = createFunName()
FunCollection[fs] = clickFun.bind(null, el, binding)
el.dataset.clickFun = fs
el.addEventListener('click', FunCollection[fs])
}
if (type === 'view') {
const vf = el.dataset.viewFun
if (vf && FunCollection[vf]) {
window.removeEventListener('scroll', FunCollection[vf])
delete FunCollection[vf]
}
const fs = createFunName()
FunCollection[fs] = viewFun.bind(null, el, binding)
el.dataset.viewFun = fs
window.addEventListener('scroll', FunCollection[fs])
FunCollection[fs](el, binding)
}
if (type === 'unload') {
if (forceRun) {
return unloadFun(el, binding)
}
const uf = el.dataset.unloadFun
if (uf && FunCollection[uf]) {
window.removeEventListener('beforeunload', FunCollection[uf])
delete FunCollection[uf]
}
const fs = createFunName()
FunCollection[fs] = unloadFun.bind(null, el, binding)
el.dataset.unloadFun = fs
window.addEventListener('beforeunload', FunCollection[fs])
}
}
// 自定义指令
Vue.directive('track', {
bind: function (el, binding) {
el.dataset.enterTime = dayjs().valueOf()
if((typeof binding.value.t === 'undefined') || binding.value.t === 'bind') {
track(el, binding)
}
},
update: function (el, binding) {
if(binding.value.t === 'update' || binding.value.act === 'unload') {
track(el, binding)
}
},
unbind: function (el, binding) {
el.dataset.leaveTime = dayjs().valueOf()
if (binding.value.act === 'unload') {
// 如果unbind时还没有unload则强制调用unload处理函数
track(el, binding, true)
} else if (binding.value.t === 'unbind') {
track(el, binding)
}
// 移除未触发的事件
const type = binding.value.act
if (type === 'click') {
const cf = el.dataset.clickFun
if (cf && FunCollection[cf]) {
el.removeEventListener('click', FunCollection[cf])
delete FunCollection[cf]
}
}
if (type === 'view') {
const vf = el.dataset.viewFun
if (vf && FunCollection[vf]) {
window.removeEventListener('scroll', FunCollection[vf])
delete FunCollection[vf]
}
}
}
})
// 处理参数
const getParams = (el, binding, occurTime) => {
const params = {
head: {
token: 'xxx', // token
sendTime: dayjs().valueOf(), // 发送时间
},
serviceDatas: [{
eventId: binding.value.id , // 事件ID
occurTime, // 事件触发时间
serviceParam: binding.value.data, // 事件数据
}]
}
return params
}
东西有点多,我简单述说一下(按顺序)。
- isInView 判断元素是否在可视区域,这个可以根据个人需要去调整,例如元素部分出现在视窗也算曝光。
isVisible = elemTop < window.innerHeight && elemBottom >= 0
即可或自行调整。 - createFunName 随机生成函数属性名,由于在多个地方都需要埋点,我们需要生成多个功能相同但名称不同的函数放在 window 下监听,并且随时移除未触发的事件。
- readyFun、clickFun、viewFun、unloadFun 各个情况触发的方法。
- track 埋点事件逻辑 click 和 scroll 就不必多说,监听点击和滚动事件。beforeunload 是页面离开前的一个事件,可以用这个替代我们前面说的路由钩子守卫。
- 自定义指令分别在bind、update、unbind调用埋点方法。比如在 unload 情况下,只有页面离开了才会触发埋点,我们需要放在 upadte 里去触发埋点方法,而不是在 bind 里一绑定就触发。再比如在 unbind 中我们需要处理一些特殊情况,如整个指令周期下来没有触发埋点方法,则要在解绑时候强行触发一次。并且要移除未触发的事件。
在页面中使用(举例):
代码语言:javascript复制<template>
<div v-track="{ act: 'unload', id: 1, data: { id: 1 }}">
content
</div>
</template>
// so on ...
上面是一个监听页面离开的埋点,离开即触发埋点行为。
act 可以取的值就是我们上述列举的几种情况:ready、click、view、unload。
id 为事件类型。
data 为附带的参数,具体看需要什么。
如果遇到指令无法完成埋点的场景,可以直接调我们开头封装的方法(trackData),不需要传入类型,直接调用即可:
代码语言:javascript复制trackData({
id: 1,
data: {
id: 1
}
})
最后
都看到这里了,不点个赞吗?
关注公众号:饼干前端,获取更多前端知识~