前端埋点简单实现方式

7,424 阅读4分钟

1.为什么使用前端埋点?

主要是为了收集产品数据,它的目的是上报相关行为数据,相关人员以数据为依据来分析产品在用户端的使用情况,根据分析出来的结果辅助产品优化、迭代、以及新需求的开发。

2.目前项目埋点方面存在的痛点?

1.逻辑复用问题:特别是曝光相关的点需要在业务代码里面做额外的处理,所以逻辑复用很困难,对现有代码的侵入也很严重;

2.埋点在多个项目中分散存在,维护会比较麻烦。

3.前端埋点方案

目前主要有 3 种方案:

1.手动代码埋点:用户触发某个动作后手动上报数据 优点:是最准确的,可以满足很多定制化的需求。 缺点:埋点逻辑与业务代码耦合到一起,不利于代码维护和复用。

2.可视化埋点:通过可视化工具配置采集节点,指定自己想要监测的元素和属性。核心是查找 dom 然后绑定事件,业界比较有名的是 Mixpanel、GrowingIO、神策 优点:可以做到按需配置,又不会像全埋点那样产生大量的无用数据。 缺点:比较难加载一些运行时参数;页面结构发生变化的时候,可能就需要进行部分重新配置。

3.无埋点:也叫“全埋点”,前端自动采集全部事件并上报埋点数据,在后端数据计算时过滤出有用数据 优点:收集用户的所有端上行为,很全面。 缺点:无效的数据很多、上报数据量大。

4.埋点方式

按照获取数据的方式一般分为三类:

页面埋点:统计用户进入或离开页面的各种维度信息,如页面浏览次数(PV)、浏览页面人数(UV)、页面停留时间、浏览器信息等。

点击埋点:统计用户在应用内的每一次点击事件,如报价列表的浏览次数、再次询价的次数等。

曝光埋点:统计具体区域是否被用户浏览到,如活动的引流入口的显示、投放广告的显示等

5.埋点方案

目前我们项目中埋点需求主要有,点击埋点(dom点击)、页面埋点(主要是pv)。再根据我们目前选用的vue技术栈,所以考虑了以下两种实现方式:组件方式或者指令方式

点击埋点开始的是想提供一个组件,包裹需要进行点击埋点的 dom 元素,也有可能是组件,然后给子元素绑定点击事件,当用户触发事件时进行埋点相关处理。

但是这样就必须绑定点击事件到 dom 上,但是又不想在文档结构中引入额外的 dom 元素,因为这会增加 dom 结构层级,层级会变得更复杂。

所以最终采用了自定义指令的方式。

6. 项目埋点实现

使用策略模式分别处理埋点需求:

/**
 * @description: 埋点方式:点击埋点、页面PV
 * @param {key} 埋点key值
 * @return {el} 点击埋点被绑定的元素
 */
const handler = {
  click (key, el) {
    el.addEventListener('click', () => {
      appPointBatchInsert(key)
    })
  },
  pv (key) {
    appPointBatchInsert(key)
  }
}

自定义埋点指令

const Point = {}

Point.install = Vue => {
  Vue.directive('point', {
    inserted (el, binding) {
      const data = binding.value
      if (data) {
        const { key, type } = data
        handler[type](key, el)
      } else {
        throw new Error('请补充正确的埋点参数')
      }
    }
  }, false)
}

export default Point

使用示例

import point from '@/utils/directive/point.js'
Vue.use(point)
<!-- 点击埋点 -->
<el-button v-point="{key: 'additionalInquiryClickKey', type: 'click'}">追加</el-button>
<!-- 页面pv埋点: section为页面根元素 -->
<section class="additional-inquiry" v-point="{key: 'additionalInquiryKey', type: 'pv'}">
...

7、之后拓展方向

曝光埋点(可能产品需求方向??) 思路:当指令第一绑定在元素上时监听目标元素,当指令从元素上解绑时停止监听目标元素。

const options = {
    root: null, //默认浏览器视窗
    threshold: 1 //元素完全出现在浏览器视窗内才执行callback函数。
}
const callback =(entries, observer) => {
  entries.forEach(entry => {})
}
const observer = new IntersectionObserver(callback, options)
const addListenner = (ele, binding) => {
 observer.observe(ele)
}
const removeListener = (ele) => {
  observer.unobserve(ele)
}
//自定义曝光指令
Vue.directive('point', {
  bind: addListenner,
  unbind: removeListener
})

注意,我们需要创建一个list将已经上报过的埋点信息记录下来,防止重复曝光。

let pointList = []; //记录已经上报过的埋点信息
const addListenner = (ele, binding) => {
 if(pointList.indexOf(binding.value) !== -1) return

 observer.observe(ele)
}

我们将要上报的信息绑定在目标元素的 'point-data' 属性中,当目标元素出现在视窗内时,并停留 5 秒以上(或者规定记录秒数时)时,上报埋点信息。

let timer = {} //增加定时器对象
const callback = entries => {
  entries.forEach(entry => {
    let pointData = null
    try {
      pointData = JSON.parse(entry.target.getAttribute('point-data'))
    } catch (e) {
      pointData = null
      console.error('埋点数据格式异常', e)
    }
    //没有埋点数据取消上报
    if (!pointData) {
      observer.unobserve(entry.target)
      return
    }

    if (entry.isIntersecting) {
      timer[pointData.id] = setTimeout(function() {
        //上报埋点信息
        sendUtm(pointData).then(res => {
          if (res.success) {
            //上报成功后取消监听
            observer.unobserve(entry.target)
            visuallyList.push(pointData.id)
            timer[pointData.id] = null
          }
        })
      }, 5000)
  } else {
    if (timer[pointData.id]) {
      clearTimeout(timer[pointData.id])
      timer[pointData.id] = null
    }
  }
  })
}

在代码中可以直接使用指令实现曝光埋点了:

<div v-point="pointData.id" :point-data="JSON.stringify(pointData)"></div>