基于 Vue directive 实现声明式埋点方案
注:本方案依赖 vue 、 lazysizes 曝光事件:lazybeforeunveil
传统埋点 vs 声明式埋点
正文开始前,对比展示一下效果,方便读者判断是否有兴趣
传统埋点
<template>
<div @click="handleMoreClick(0)">More</div>
</template>
<script>
export default {
methods: {
handleMoreClick(type) {
this.onAnalyticsEvent('查看更多', '签到活动 card【查看更多】_click', 'b_4ezhmbjt', { type })
},
onAnalyticsEvent(element_id, title, bid, lab) {
AnalyticsUtil.sendAnalyticsEvent({
val: {
element_id,
title,
bid,
lab,
}
})
},
}
}
</script>
声明式埋点
<template>
<div v-mge:b_4ezhmbjt="0">More</div>
</template>
<script>
/* 无需写其他代码 */
</script>
神奇的 v-mge
如何实现,埋点信息如何获取? 请看下文分解~
背景
- 前端页面埋点需求很多,基本采用传统的命令式埋点,随着项目业务需求开发持续进行,出现越来越多的冗余代码,虽做了部分公共封装,但与业务逻辑无关的埋点代码还是不可逆地累积,且分散在源码各处。
- 埋点过程有较多重复性劳动,低效率且容易出错。
团队埋点流程:产品配置埋点->开发从平台逐个 copy 埋点信息->粘贴到源码->运行时发送埋点。
调研
- 接收到优化埋点的任务时开始调研,准备了两套方案:a: 可视化无痕埋点;b: 声明式埋点。
- 方案一最终流产。不过多介绍,原因总结如下:
- 重新制定了埋点流程,需多方配合
- 开发工作量大且复杂
虽可提供动态埋点功能,但产品对现有埋点流程无痛感,无动力使用新方案。
所以,为解决自己的问题,期望别人做太大改动是不切实际的。
- 声明式埋点依赖
vue
框架,可以提升效率,仍需要维护埋点信息(按页面统一维护),但实现起来足够简单,且对流程无影响。
实现
资源依赖如图:
通过 Chrome 插件(推荐 chrome 插件,可快速写写浏览器 tampermonkey )生成 mge data(这部分是团队定制的,不作介绍)。
// 生成的数据样例
export default {
b_4ezhmbjt: type => ({
elementName: '查看更多',
eventName: '签到活动 card【查看更多】_click',
eventType: 'click',
custom: { type },
}),
// ...
}
在页面(page.vue)导入埋点数据、注册指令。在根节点订阅(mgeSubscriber)指令发布的消息,所有埋点事件触发后都将埋点数据传递给该订阅者。
数据流向:
- 指令(v-mge)初始化时给 dom 节点绑定事件。
- 事件触发时根据埋点 id 获取对应的埋点信息,加上业务参数合成完整的埋点数据。
- 完整的埋点数据传递给 Vue 实例根节点提供(Provide)的
mgeSubscriber
。 mgeSubscriber
接收到数据后上报到埋点服务器。
收益
- 一个埋点大约能节约 1~2 分钟
- 一个埋点大约 5 行代码,精简到可以忽略
- 埋点数据从业务代码中剥离出来(单独文件管理,import 到页面)
- 覆盖 90%以上的场景(UI 组件内事件需要在 handler 中上报,不能通过指令埋点)
详细 API
<template lang="html">
<!-- 携带一个业务字段事件 -->
<div v-dr-mge:b_5ix7ve3c="title">
{{ a }}
<!-- 携带多个业务字段 -->
<div v-dr-mge:b_7sslet2v="[title, shopId]">
{{ b }}
</div>
<!-- 不携带业务参数事件 -->
<div v-dr-mge:b_vyc33sw0></div>
<div v-for="(v, index) in arr">
<!-- 曝光+click 事件 -->
<p v-dr-mge:b_1rlrj8dr|b_6zn0e86f="[v, index]">{{v}}</p>
</div>
</div>
</template>
可参考的指令源码
import 'lazysizes';
// 每个埋点传递进来的值,经过 MGE_DATA 转换后的结果,当事件触发时将结果发送给 provide
const CACHE_DATA = {};
function handleMge(el, bidKey, e) {
// click 使用 el,曝光使用 e.target
const uniqBid = (el || e.target).dataset[bidKey];
const mgeInfo = CACHE_DATA[uniqBid];
if (!mgeInfo) return;
const { data, subscriber } = mgeInfo;
data.bid = uniqBid.replace(/-\d+$/, '');
subscriber(data);
}
// 绑定 lazysizes 提供的 lazybeforeunveil 事件
document.addEventListener('lazybeforeunveil', handleMge.bind(null, null, 'viewMgeBid'));
// 判断传递给指令的新值、旧值是否相等
function directiveValueEquals(v1, v2) {
if (v1 === v2) return true;
if (typeof v1 !== typeof v2) return false;
return v1.toString() === v2.toString();
}
// 生成唯一值,解决一个 bid 被注册多次的场景。如在 v-for 生成的元素上使用 dr-mge
const uniq = (() => {
let id = 0;
return bid => {
id += 1;
return `${bid}-${id}`;
};
})();
/**
* 创建 mge 指令
* @param {object} mgeData 从 ocean 获取的埋点数据
* @return {object} vue 指令
*/
export default function createMgeDirective(mgeData) {
if (mgeData == null || typeof mgeData !== 'object') return;
// eslint-disable-next-line
return {
inserted(el, binding, vnode) {
const { arg, value } = binding;
const { $mgeSubscriber } = vnode.context.$root._provided;
// 如果根节点未提供处理 mge 事件的 handle,则忽略该指令
if (!$mgeSubscriber) {
console.error('root 节点 provide 没有$mgeSubscriber');
return;
}
arg.split('|').forEach(bid => {
const mgeInfo = mgeData[bid];
if (!mgeInfo) {
console.error('未注册埋点信息的 bid:', bid);
return;
}
const uniqBid = uniq(bid);
const eventData = typeof mgeInfo === 'function' ? mgeInfo(...[].concat(value)) : mgeInfo;
el.setAttribute(`data-${eventData.eventType}-mge-bid`, uniqBid);
CACHE_DATA[uniqBid] = {
data: eventData,
subscriber: $mgeSubscriber, // 当 mge 事件触发时将 data 传递给 handle
};
if (eventData.eventType === 'click') {
el.addEventListener('click', handleMge.bind(null, el, 'clickMgeBid'));
} else if (
eventData.eventType === 'view'
&& !el.classList.contains('lazyload')
) {
el.classList.add('lazyload');
}
});
},
componentUpdated(el, binding) {
const { arg, value, oldValue } = binding;
arg.split('|').forEach(bid => {
// 从 dataset 中查找 uniqBid
const uniqBid = Object.keys(el.dataset)
.map(key => el.dataset[key])
.find(v => new RegExp(`${bid}-\\d+$`).test(v));
if (!uniqBid) return;
const mgeInfo = mgeData[bid];
const cacheData = CACHE_DATA[uniqBid];
if (mgeInfo && cacheData) {
// 更新缓存值
cacheData.data = typeof mgeInfo === 'function' ? mgeInfo(...[].concat(value)) : mgeInfo;
// 曝光事件,当传递的参数改变时需要重新曝光,重置 class
if (cacheData.data.eventType === 'view' && !directiveValueEquals(value, oldValue)) {
el.classList.remove('lazyloaded');
el.classList.add('lazyload');
}
} else {
console.error('指令更新异常,未找到该埋点信息或缓存数据', bid);
}
});
},
unbind(el, binding) {
binding.arg.split('|').forEach(bid => {
delete CACHE_DATA[bid];
});
},
};
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 两个链表的第一个公共结点
下一篇: 谈谈自己对于 AOP 的了解
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论