https://zhuanlan.zhihu.com/p/27659302html
本文介绍了一种通用的前端埋点方案的设计和实现,具备适配项目普遍,易于使用,与业务逻辑解耦等优势,已经在外卖商业平台进行了一段时间的试用,并取得良好效果。前端
销售CRM方向是外卖为销售人员提供各维度的工具和平台,以帮助提升销售人员工做的效率。在销售CRM方向的PC端,一直没有对用户行为的数据采集(即埋点数据采集),因此对于分析用户行为、观察产品使用情况、制定产品策略等都缺少相关的数据支持。jquery
因此在今年3月份销售CRM方向决定启动PC端的各方向的埋点,包括智子、任务制、HES、商机等多个系统。PM整理的埋点个数达到了100多个。git
在埋点的后端方案采用DA的SAK。而在前端方向,这几个系统有使用jquery+widget的老方案,也有基于Vue的技术栈实现。须要如何埋点?怎样实现简单高效的埋点?这是须要咱们解决的问题。github
业界的埋点方案主要分为如下三类:后端
在当时排期紧凑,人力紧缺的状况下,显然不容许咱们去开发可视化埋点方案和无埋点方案,因此只能采起代码埋点方案。浏览器
代码埋点分为 命令式埋点 与 声明式埋点 。app
命令式埋点,顾名思义,开发者须要手动在须要埋点的节点处进行埋点。如点击按钮或连接后的回调函数、页面ready时进行请求的发送。你们确定都很熟悉这样的代码:框架
// 页面加载时发送埋点请求
$(document).ready(function(){
// ... 这里存在一些业务逻辑
sendRequest(params);
});
// 按钮点击时发送埋点请求
$('button').click(function(){
// ... 这里存在一些业务逻辑
sendRequest(params);
});
能够很容易发现,这样的作法颇有可能会将埋点代码侵入业务代码,这使总体业务代码变得繁琐,容易出错,且后续代码会越发膨胀,难以维护。因此,咱们须要让埋点的代码与具体的业务逻辑解耦,即 声明式埋点 ,从而提升埋点的效率和代码的可维护性。dom
理论上,声明式埋点只须要关注两个问题:
所以,能够很快想出一个声明式埋点的方法:
// key表示埋点的惟一标识;act表示埋点方式 <button data-stat="{key:'111', act: 'click'}">埋点</button>
那么能够去遍历DOM树,找到 [data-stat] 的节点,给这个button绑上click事件,把这些参数在回调函数中经过请求发出去。
在DOM节点(html)上声明埋点,与业务逻辑(一般在Javascript文件中)就解耦了。调用也很方便。
看起来很美,但这样就能解决问题了吗?显然是不够的。还须要解决如下问题:
回顾一下,咱们须要解决的问题是:
咱们最终提出了一个基于Vue指令(Directive)和混合(Mixin)的解决方案:
因为在埋点的需求中有部分项目使用了Vue做为基础框架,结合上面声明式埋点的例子,很容易就联想到 Vue自定义指令。Vue自定义指令提供了一种机制,将数据的变化映射为 DOM 行为。以 Vue 1.x 版本为例,自定义指令提供了几个钩子函数:
- bind:只调用一次,在指令第一次绑定到元素上时调用。
- update: 在 bind 以后当即以初始值为参数第一次调用,以后每当绑定值变化时调用,参数为新值与旧值
- unbind:只调用一次,在指令从元素上解绑时调用
这样的特性能够很好的解决以上的一些问题。咱们只须要像这样:
Vue.directive('stat', { bind: function () { // 准备工做 }, update: function (newValue, oldValue) { // 值更新时的工做 // 也会以初始值为参数调用一次, 此时能够根据传值类型来进行相应埋点行为的请求处理 }, unbind: function () { // 清理工做 } })
在一个Vue应用中,不须要再去遍历DOM树,由于在Vue应用中基本全部DOM操做都是使用数据的变动结合Vue的内置指令实现,Vue能够感知到这些变动。在指令从元素上解绑时咱们也能够去销毁已经绑定的事件。
那么接下来的问题是,还有一些项目基于 jquery + widget 的老方案实现,那么在这些项目中的DOM操做是jquery甚至原生DOM API来实现,Vue的自定义指令就没法工做。举个例子:
<div id="container"> <button id="btn">click</button> </div> <script> new Vue({ el: '#container', directives: {stat} }) $('#btn').click(function() { $('#container').append('<button v-stat="{key: '3', act: 'click'}">click</button>') }) </script>
在上面例子中,虽然Vue已经挂载到 container 容器上,引入了自定义指令stat, #btn 这个按钮点击时插入了一段带有指令v-stat的按钮,由于Vue没法感知这个DOM变动,因此该指令不能被解析。这样的方式就会失效。
以前在外卖运营平台方向有基于 jquery 的DOM劫持操做的实现,在全部DOM操做中加入埋点相关的逻辑;由于没法保证全部的DOM操做都使用 jquery , 且不能保证全部埋点逻辑彻底一致,因此也没法通用。
那么,怎样保证在任意库,包括原生API的DOM操做下都感知到DOM的变动而且通知Vue从新解析指令呢?这里就须要引入 MutationObserver。
MutationObserver是在DOM3标准中提出的标准API,提供让开发者感知到在某一个DOM节点变动的能力。能够监听如下场景:
MutationObserver的浏览器支持状况已经比较好了.
&lt;img src="https://pic4.zhimg.com/v2-1419467a7f6369f269fe3977f097bcc2_b.jpg" data-rawwidth="1490" data-rawheight="152" class="origin_image zh-lightbox-thumb" width="1490" data-original="https://pic4.zhimg.com/v2-1419467a7f6369f269fe3977f097bcc2_r.jpg"&gt;但为了保证MutationObserver能够在全部浏览器上正常工做,咱们仍然引入了这个API的polyfill,详情可见这里。
在此能力的前提下,咱们就能够在任意的DOM操做下触发Vue进行从新解析指令。
咱们将 MutationObserver 封装进一个 Vue mixin , 非Vue应用的业务代码只须要引入这个mixin,这样也能够很好地解耦。
详细的实现原理能够见如下伪代码:
let observer; export default { ready() { // 开启监听 observer = new MutationObserver(mutations => { this.$compile(this.$el); }); observer.observe(this.$el, config); }, destroyed() { // 清理工做 observer.disconnect(); observer.takeRecords(); } }
关于MutationObserver的详细介绍请见 标准文献。
埋点库另外一部分主要的逻辑是处理埋点行为。
Ready事件的处理,在页面根元素绑定指令后,在指令第一次update钩子调用时便可认为该元素ready, 直接发起请求埋点便可;
click事件的处理,在该节点上绑定click事件,在指令解绑时销毁该事件。
区域展示埋点即:当区域为可见状态变动时进行埋点。
那么,咱们一样须要监听节点的可见状态变动。
理论上,DOM可见状态的变动也在MutationObserver的监听范围内,最初的一种思路是:
let observer = new MutationObserver((mutations) => { if (mutations[0].oldValue.indexOf('display: none') > -1 && mutations[0].target.style.display !== 'none') { sendRequest(); } }) let config = { attributes: true, attributeOldValue: true, attributeFilter: ['style'] }; observer.observe(el, config);
可是这种思路很快被否决,由于很显然,可见状态还有多是被节点类名class控制的。而具体节点上的类名是没法预期的,所以这种方案行不通。
最终咱们使用了开源库 VisSense。VisSense提供了监听可见状态变动的能力,具体请见这里,本文不进行详细描述。
VisSense 实际使用了消息订阅模式和setInterval来进行周期性的节点状态检查,感兴趣的同窗能够看看它的源码。
因而在这里咱们就能够进行很方便的可见状态监听:
function handleShow(el) { var visMonitor = VisSense(el).monitor({ visible: function() { sendRequest(); } }); visMonitor.start(); }
眼球曝光埋点标识用户是否「看到」了某个区域,那么用前端的方式来解释就是:
主要的实现思路就是监听scroll事件,与当前节点的scrollTop进行对比。
因为本次需求未涉及眼球曝光,本部分再也不赘述。
上面的声明式埋点方案已经能够解决大多数问题。
可是,不是100%的状况都适用声明式埋点,主要发生在 DOM操做不受开发者彻底控制 的状况。
举个例子,在使用百度地图API时,在地图上打一些POI点(markPoint), 或者一些蒙层(如Polygon), 再在点击这些覆盖物时埋点,因为这些DOM操做是百度地图API完成的,没法预期插入了哪些DOM,天然就不能在这些DOM上插入指令。因此只能在调用API时进行命令式埋点。须要咱们也提供命令式埋点支持。
命令式埋点的大部分逻辑实际已经包含在指令中,因而咱们在指令中提供了这样的接口方式:
export default { bind() {...}, update() {...}, unbind() {...}, sendStat(val) { // 命令式埋点接口 } }
引入此模块后,便可以看成Vue指令使用,也能够当作一个API来使用。
此外,埋点方案还提供了可配置能力,能够设定测试环境仍是生产环境的规则(根据URL匹配),设定埋点请求的URL地址,是否开启debug模式等。
在测试环境下,埋点请求的时机只会在浏览器中进行console.log并打印出触发埋点的节点,不会实际发送请求,能够支持测试环境下的正常开发,又能够避免埋点出现脏数据。
new Vue({ el: '#app', // 根节点 directives: {stat}, mixins: [observerMixin] // 非Vue项目须要引入 })
而后在页面相应节点进行声明式埋点便可:
<div id="app" v-stat="{'act':'ready',' key':'samplepg'}"></div> // 页面展示埋点
<button v-stat="{'act':'click', 'key':'samplebtn'}"></button> // 点击统计埋点
<div id="container" v-stat="{'act':'show',' key':'samplepn'}"></div> // 区域展示埋点
这样的埋点方式十分简便快捷。
在实际业务开发过程当中,本埋点方案平滑适配了Vue项目和jquery等开发的一些老项目,能够很好地和业务代码解耦,只须要在须要埋点的DOM节点上进行声明式埋点,开发简单高效,在排期人力紧张的状况下,很好地支持了100余个埋点数据统计。
前端的数据采集和上报是构建数据平台的重要环节,而前端如何进行埋点也是值得深究的。为了快速知足业务的大量埋点需求,咱们使用了本文的埋点方案,并且已经大量在商业平台部开发中使用,不管从FE同窗的开发反馈、实际产出数据的结果来看都达到咱们的预期,后续会继续在一些业务上进行持续迭代和优化。