uniapp+vue自定义埋点数据收集和提交

3,190 阅读4分钟

公司开发了一款为高考学生服务的APP,技术站选用的uniapp+vue全家桶,是一款非常实用的高考志愿管理服务应用软件。开发后期领导新提出需求,需要自己写事件埋点,自己统计PV、UV数据。经过不懈的加班抠唆出了些许的优化代码,在此做个记录:

首先是明确需要收集什么信息和什么时候收集信息:经过和相关同事探讨后我们一致通过了一下几个字段为必要数据

收集什么信息?

  // 规定传参的字段
  const dataOrder = [
    'timeStamp',
    'uuid',
    'appChannel',
    'eventType',
    'path',
    'loginType',
    'userId',
    'appVersion',
    'netType'
  ]

timeStamp是时间戳,uuid是设备的唯一标识,appChannel是app的下载渠道,eventType是该数据的事件类型(页面/事件),path页面的路径,loginType登录状态(0非登录;1登录),userId用户的唯一标识,appVersion是app的版本号,netType是网络类型。

什么时候收集发送信息?

上述信息中大多都和设备有关系并且收集一次足够,所以需要咱们在onLaunch应用生命周期也就是当uni-app 初始化完成时(全局只触发一次)完成大多数的数据收集,收集到数据之后存储到vuex状态状态管理中,方便在全局其他地方使用,其中'timeStamp'、'timeStamp'、'eventType'、'path'、'loginType'是时效性强的数据需要再传输的时候在获取。

发送信息的时机分两种情况,一种是进入页面显示内容的时候,另外一种就是事件类触发的时候,比如app的显示和隐藏、点击按钮的触发等,事件类一定是触发的时候发送信息,那进入页面的时候需要在每个页面都写一遍发送方法吗?这样既不美观也不好维护,容易漏写忘记,显然不是一个好的办法。这个时候想起vue的mixin,可以动态的注册组件的生命周期,等后边实现的时候再看代码实现

// 事件类型 是事件还是页面
const _S_TYPE = {
  EVENT: 'event',
  PAGE: 'page'
}

代码实现

基础信息罗列清楚后就开始撸代码吧

首先单独弄一个统计的statistics模块

/**
 * index.js
 * 统计埋点的所有方法
*/
import store from '@/store/index.js'
import Api from '@/api/login/login.js' // 获取用户数据的一个接口
import { sendStatData } from '@/api/statistics/index.js' // 发送统计数据的接口

// 规定传参的字段
const dataOrder = [
  'timeStamp',
  'uuid',
  'appChannel',
  'eventType',
  'path',
  'loginType',
  'userId',
  'appVersion',
  'netType'
]
// 事件类型 是事件还是页面
const _S_TYPE = {
  EVENT: 'event',
  PAGE: 'page'
}
// 是否初始化数据了
let initData = false

// 判断启动页index页面的四个页面
// 因为首页是一个页面,底下四个tab按钮来回切换其中的四个组件,所以需要单独处理成页面后再发送给后端
const indexPages = {
	home: {
		path: 'pages/home/home'
	},
	college: {
		path: 'pages/college/college'
	},
	mine: {
		path: 'pages/mine/mine'
	},
	zixun: {
		path: 'pages/consulting-list/consulting-list'
	}
}

// 获取处理当前页面的路径url
function getPageFullPath () {
  let fullPath = ''
  let pages = getCurrentPages() // uniapp的全局方法 函数用于获取当前页面栈的实例
  if (pages.length > 0) {
    let path = pages[pages.length - 1].route
    //获取路由参数
    let options = pages[pages.length - 1].options || pages[pages.length - 1].$route.query
    let params = ''
    // 拼接参数
    if(options && JSON.stringify(options) != '{}') {
      Object.keys(options).forEach((key, index) => {
        let beforeStr = ''
        if (index === 0 ) {
          beforeStr = '?'
        } else {
          beforeStr = '&'
        }
        params += beforeStr + key + '=' + options[key]
      })
    }
    fullPath = path + params
  }
  return fullPath
}
// 获取设备信息 uuid
function getDeviceInfo () {
    plus.device.getInfo({
      success: function(e) {
        store.dispatch('setUuid', encodeURIComponent(e.uuid))
      },
      fail: function () {
        console.log('获取uuid失败')
      }
    })
}
// 获取app渠道
function getAppChannel (channel) {
  let channelStr = ''
  if (channel === 'ios') {
    channelStr = 'ios'
  } else {
    channelStr = plus.runtime.channel
  }
  store.dispatch('setAppChannel', channelStr)
}
// 获取app版本号
function getAppVersion () {
  store.dispatch('setAppVersion', plus.runtime.version)
}
// 获取网络类型
function getNetType () {
  uni.getNetworkType({
    success: function (res) {
      store.dispatch('setNetType', res.networkType)
    }
  })
}
// 整合所有数据
function planData (type, path) {
  let data = store.getters.statData
  let obj = {
    timeStamp: new Date().getTime(),
    eventType: type,
    path: encodeURIComponent(path)
  }
  let objNew = {...obj, ...data}
  let str = ''
  dataOrder.forEach((item, index) => {
    let afterStr = ','
    if (index === dataOrder.length - 1) afterStr = ''
    str += objNew[item] + afterStr
  })
  return str
}

// 初始化的时候获取统计的相关数据 app.vue调用
export function getStatisticsInfo (channel) {
  getDeviceInfo()
  getAppChannel(channel)
  getAppVersion()
  getNetType()
  getUserShareCode()
  initData = true
}

// 最终的发送数据事件  把数据传发送给服务端 
// event表示事件类型, eventname表示事件名称
export function sendStatisticsData (event, eventName) {
  if (!initData) return
  let path = event === _S_TYPE.EVENT ? eventName : eventName ? eventName : getPageFullPath()
  // 如果是页面事件 并且没有值 则认为是start页面 不用传值
  if (event === _S_TYPE.PAGE && !path) return
  // 如果是启动页则判断是tabbar中的那个组件在显示
  if (path === 'pages/index/index') {
    let tabType = store.state.tabbar.curTab
    path = indexPages[tabType].path
  }
  let data = planData(event, path)
  sendStatData(data)
}

// 暴露出去统计类型 页面还是事件 方便全局使用
export let S_TYPE = _S_TYPE

// 清除统计用户登录数据 登出的时候用
export function resetStatData () {
  store.dispatch('resetStatData')
}

// 获取用户登录状态和分享码作为用户唯一id
export function getUserShareCode () {
  // 如果统计数据中没有个人用户数据则收集数据  登录时和 onLaunch时
  if (!store.state.statistics.statData.userId && store.state.statistics.statData.loginType === 0) {
    Api.getUser().then(res => {
      const { code, data } = res
      if (code === 1) {
        if (data) {
          // 缓存shareCode为页面统计中的userId 用户唯一标识
          store.dispatch('setUserId', data.shareCode)
        }
      }
    })
    .catch(err => {
      console.error(err)
    })
  }
}

其次呢当中用了很多次vuex的dispatch,代码就是简单的存储


/**
 * 自定义统计埋点保存的变量
 */

let app = {
  state: {
    statData: {
      uuid: '', // 设备的uuid
      userId: '', // 用户唯一标识
      loginType: 0, // 登录状态 0 未登录 1 登录  默认是未登录
      netType: '', // 网络类型
      appVersion: '', // app版本号
      appChannel: '' // app的渠道
    }
  },
  getters: {
    statData: state => state.statData
  },
  mutations: {
    SET_STAT_UUID(state, id) {
      state.statData.uuid = id
    },
    SET_STAT_USERID(state, id) {
      if (state.statData.userId && state.statData.loginType === 1) return
      state.statData.userId = id
      state.statData.loginType = 1
    },
    SET_STAT_NETTYPE(state, type) {
      state.statData.netType = type
    },
    SET_STAT_VERSION(state, type) {
      state.statData.appVersion = type
    },
    SET_STAT_CHANNEL(state, type) {
      state.statData.appChannel = type
    },
    // 重置统计数据
    RESET_STAT_DATA(state) {
      state.statData.userId = ''
      state.statData.loginType = 0
    }
  },
  actions: {
    setUuid ({ commit }, id) {
      commit('SET_STAT_UUID', id)
    },
    setUserId ({ commit }, id) {
      commit('SET_STAT_USERID', id)
    },
    setNetType ({ commit }, type) {
      commit('SET_STAT_NETTYPE', type)
    },
    setAppVersion ({ commit }, str) {
      commit('SET_STAT_VERSION', str)
    },
    setAppChannel ({ commit }, str) {
      commit('SET_STAT_CHANNEL', str)
    },
    resetStatData ({ commit } ) {
      commit('RESET_STAT_DATA')
    }
  }
}
export default app

重点是怎么轻松的把页面展示事件融入到项目的每一个页面当中并且把相关事件暴露到全局让埋点事件随时随处可用


/**
 * mixin-life.js
 * 封装一个vue插件
*/
import { sendStatisticsData, S_TYPE, getUserShareCode } from './index.js'

// 把统计埋点注入到页面的生命周期
export default {
  install (Vue) {
    Vue.mixin({
      // uniapp中页面都有onShow事件,表示页面每次出现在屏幕上都触发,如果用mounted事件则只会初始化的时候有再次展示的时候没有
      onShow () {
        sendStatisticsData(S_TYPE.PAGE)
      }
    })
    // 将方法和属性进行全局注册
    // 发送统计数据的最终方法
    Vue.prototype.$sendStatisticsData = sendStatisticsData
    // 统计的点击或者页面事件类型
    Vue.prototype.$S_TYPE = S_TYPE
    // 获取用户的分享码作为用户唯一标识
    Vue.prototype.$getUserShareCode = getUserShareCode
  }
}


这样封装以后页面展示PV不用添加额外代码,事件触发类事件则在相应的地方调用即可: 使用方法如下:


// APP.vue
// 统计数据函数 这个函数只在onLaunch中调用一次所有没有必要挂载在vue实例上,单独引入即可
import { getStatisticsInfo } from '@/common/statistics/utils.js'
onShow() {
  // ...
  this.$sendStatisticsData(this.$S_TYPE.EVENT, 'app_show')
},
onHide() {
  // ...
  this.$sendStatisticsData(this.$S_TYPE.EVENT, 'app_hide')
},
onLaunch() {
  // ...
  // #ifdef APP-PLUS
  // 搜集统计用的数据
  getStatisticsInfo(this.appplatform)
  // #endif
},

中间有一个小插曲,一般而言在web端埋点发送都是用1px*1px的图片去传输数据,但因为这个项目是app项目在app中是没有dom一说的,不能动态添加图片,所以最后选择的是uniapp自带的AJAX的api:uni.request,但是有次埋点服务报红,从而影响了app的其他正常逻辑,这个肯定不能忍。经过调研最后用H5+的请求api来发送埋点数据,应该和uni.request是不同的线程所以不受影响


import { STAT_API_URL } from '@/utils/urls.js'

/**
 * 发送统计数据
 */

export function sendStatData (val) {
  // 用uni.request如果请求失败或延迟会影响其他后续的请求 改成H5+的方式
  let xhr = new plus.net.XMLHttpRequest()
  xhr.onreadystatechange = function () {
  	switch ( xhr.readyState ) {
  		case 0:
  			// console.log( "xhr请求已初始化" );
        break;
  		case 1:
  			// console.log( "xhr请求已打开" );
        break;
  		case 2:
  			// console.log( "xhr请求已发送" );
        break;
  		case 3:
  			// console.log( "xhr请求已响应");
  			break;
  		case 4:
  			if ( xhr.status == 200 ) {
  				// console.log( "xhr请求成功:"+xhr.responseText );
  			} else {
  				// console.log( "xhr请求失败:"+xhr.readyState );
  			}
  			break;
  		default :
  			break;
  	}
  }
  xhr.open("GET", `${STAT_API_URL}/statistics?v=${val}&t=${new Date().getTime()}`)
  xhr.send()
}

最后简单总结一下就是:

尽可能细的处理各个信息,最后封装到一起

使用vue的mixin混入更加方便快捷的传达信息到每个页面每个组件