为了感知用户的行为,并基于用户使用产品的各种行为,进行产品的迭代优化等。项目代码中有大量的埋点,主要是页面浏览
和元素点击
两种。
按理说,这也是一个业务无关的逻辑,不应该出现在业务代码中,就算必须嵌入业务代码,也应该占据很少的一部分,但事实上,埋点不但侵入了业务代码,而且占据了非常多的代码量。
现场是这样的,感受一下。
点击事件埋点:
timer(x = this) {
this.$logger.log({
type: this.$logger.eventType.action,
event_id: Date.now().toString(),
payload: [
{
key: 'type',
value: 'time'
}
],
event: 'quick_tool_click',
event_name: this.$logger.getEventName('quick_tool_click')
})
//使用默认值,把this方便传给vuex,方便埋点
this.showTimer(x)
},
redflag() {
this.$logger.log({
type: this.$logger.eventType.action,
event_id: Date.now().toString(),
payload: [
{
key: 'type',
value: 'redflag'
}
],
event: 'quick_tool_click',
event_name: this.$logger.getEventName('quick_tool_click')
})
if (this.selectedStudentList.length == 0) {
return this.toast2()
} else {
this.$router.push({
name: coverPageNameToLowerCase(PageName.GROUPING_GRANT.ROUTE),
query: {
from: PageName.LECTURE.ROUTE
}
})
}
},
页面浏览埋点:
beforeRouteEnter(to, from, next) {
next(vm => {
vm.pv_id = Date.now().toString()
vm.$logger.log({
type: vm.$logger.eventType.pv,
event_id: vm.pv_id,
page_name: PageName.CLASS_ENJOY.LOG,
event: 'page_entry',
event_name: vm.$logger.getEventName('page_entry')
})
})
},
beforeRouteLeave(to, from, next) {
this.$logger.log({
type: this.$logger.eventType.pv,
event_id: this.pv_id,
page_name: PageName.CLASS_ENJOY.LOG,
event: 'page_leave',
event_name: this.$logger.getEventName('page_leave')
})
next()
}
这两段代码,第一段是点击事件埋点,第二段是页面浏览埋点。很常见的一个需求,却也很常见的大段代码,这显然是不合理的。
经过分析,我发现了几个问题:
- 很多参数是没必要传的,完全可以封装到log内部,比如自动生成的event_id,比如从map中取的event_name。
- 像页面浏览,每个页面都要加一个beforeRouteEnter,beforeRouteLeave,太繁琐了,而且一旦有改动,那需要改动的文件数量也特多。
这两点问题是两个方面,一个是内部的封装(内忧),一个是外部的使用(外患),一个是部分参数应该封装到内部,一个是外部的使用方式应该简化。
我分别对这两点进行了改进,第一点可以把内部暴露出的方法简化,第二点可以用mixin或指令来解决,这里指令的方式更加优雅。
之前暴露出的接口:
const _logger = function (Vue, options) {
if (_logger.installed) {
return;
}
const tracker = logger.getLogger('tracker');
Object.defineProperties(Vue.prototype, {
$logger: {
get() {
return {
log: function (data) {
var _log = configContext(tracker, options);
_log.info(data)
},
getEventName: function (key) {
return _eventmap.get(key);
},
eventType: {
pv: 'page_view',
action: 'action',
profile: 'performance'
}
}
}
}
})
}
我把log改成了私有方法,暴露出了trackPv,trackAction和trackPerformance三个方法。参数也精简成了:pageName、eventName、trackData,简洁却又足够。
const tracker= logger.getLogger('tracker');
const eventType = {
pv: 'page_view',
action: 'action',
performance: 'performance'
}
//这里叫eventName是不对的,应该是eventDesc,是描述信息
const getEventDesc = function (key) {
return _eventmap.get(key);
}
const log = function (data) {
var _log = configContext(tracker, options);
_log.info(data)
}
const idGenerator = {
timeId() {
return Date.now().toString()
}
}
const track = (eventType, pageName, eventName, eventData) => {
log({
event_id: idGenerator.timeId(),
type: eventType,
page_name: pageName,
event: eventName,
event_name: getEventDesc(eventName),
payload: eventData
});
}
const eventTracker = {
trackPv(pageName, eventName, eventData) {
track(eventType.pv, pageName, eventName, eventData);
},
trackAction(pageName, eventName, eventData) {
track(eventType.action, pageName, eventName, eventData);
},
trackPerformance(pageName, eventName, eventData) {
track(eventType.performance, pageName, eventName, eventData);
}
};
Object.defineProperties(Vue.prototype, {
$tracker: {
get() {
return eventTracker;
}
}
})
现在组件内的调用方式:
this.$tracker.trackPv('login', 'login_btn_click', {
a: 'aaa',
b: 'bbb'
});
这样还不够,比如页面浏览,埋点还是得写在组件里的路由切换的钩子里。于是我又封装了v-trakPv,v-trackClickAction两个指令,前者加在页面组件上,后者用在具体元素上。
trackPv:
/**
* trackPv指令
* 绑定元素在组件创建销毁时会进行 page_entry和page_leave埋点
*
*/
export default (tracker) => {
const handleTrackInfo = (value) => {
const trackInfo = value || {};
if(!trackInfo.pageName) {
throw new Error('trackPv指令: pv埋点必须传入pageName');
}
return trackInfo;
}
return {
bind(el, binding) {
const {pageName, eventData} = handleTrackInfo(binding.value);
tracker.trackPv(pageName, 'page_entry', eventData);
},
unbind(el, binding) {
const {pageName, eventData} = handleTrackInfo(binding.value);
tracker.trackPv(pageName, 'page_leave', eventData);
}
}
}
trackClickAction:
/**
* trackClickAction指令
* 绑定元素点击时会埋点
*
*/
export default (tracker) =>{
let clickHandler = () => {}
return {
bind(el, binding) {
const {pageName, eventName, eventData} = binding.value || {};
clickHandler = () => {
tracker.trackAction(pageName, eventName, eventData);
}
el.addEventListener('click', clickHandler);
},
unbind(el, binding) {
el.removeEventListener('click', clickHandler);
}
}
}
这样,就把pageview埋点的所有代码都封装到了指令内部,组件里只需要:
<div v-trackPv="{pageName: 'aaa', eventData: {}}"></div>
而元素点击的埋点也可以简化成:
<button v-trackClickAction="{pageName:'login', eventName: 'login_btn_click',eventData:{}}">登陆</button>
这两种情况之外,与业务有关的埋点,或者其他事件的埋点,可以手动调用api
this.$tracker.trackAction('pageName', 'event_name ', {data: 'data'});
组件中的业务无关代码量,得到了非常大的减少。都转移到了模板中的指令里。
总结
埋点是很常见的需求,有些是业务无关、有的是业务相关。业务无关的埋点不应该出现在组件代码里,业务相关的埋点调用方式也应该简单。
所以,我首先部分解决了内忧,把暴露的api进行了精简,同时也针对外患,也就是调用方式的繁杂,通过指令进行了封装。