前端埋点黑科技

19,850

前言

最近埋点业务接触的不少,于是乎想整理一篇相关的文章出来,分享给大家,也便于自己阅读。

由于是使用 vue2.x 实现的业务,所以埋点是基于vue2.x来的(什么技术栈不重要逻辑是一样的)。

如果是自己想玩一下,可以使用百度的埋点统计(npm包 vue-ba): 传送门

埋点

如果是内部自己的埋点统计,需要理清一下埋点触发的几种时机:

  • ready: 进入指定页面时触发
  • click: 点击指定元素时触发
  • view: 指定区域眼球曝光时触发
  • unload: 离开指定页面时触发

埋点

进入指定页面触发埋点是很常见的埋点行为,最简单的方式就是在路由守卫调取埋点接口即可。但是为了不在每个页面的路由守卫重复书写,我们可以统一抽取封装埋点行为。

这里用到两个比较常见的工具库:dayjs、underscore(不用也可以,看个人)

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 的话怎么触发呢?因为涉及到元素位置等一系列问题,所以我们再封装一个方法来解决这个问题,一劳永逸。

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 中我们需要处理一些特殊情况,如整个指令周期下来没有触发埋点方法,则要在解绑时候强行触发一次。并且要移除未触发的事件。

在页面中使用(举例):

<template>
    <div v-track="{ act: 'unload', id: 1, data: { id: 1 }}">
        content
    </div>
</template>
// so on ...

上面是一个监听页面离开的埋点,离开即触发埋点行为。

act 可以取的值就是我们上述列举的几种情况:ready、click、view、unload。

id 为事件类型。

data 为附带的参数,具体看需要什么。

如果遇到指令无法完成埋点的场景,可以直接调我们开头封装的方法(trackData),不需要传入类型,直接调用即可:

trackData({
    id: 1,
    data: {
        id: 1
    }
})

最后

都看到这里了,不点个赞吗?

关注公众号:饼干前端,获取更多前端知识~