好玩的JS系列--小程序数据埋点
前言
最近负责的一个项目,项目的客户端是微信小程序。客户提了需求,需要进行数据埋点,收集用户行为数据,为后续提升用户体验及产品优化提供基础数据收集。于是在一个风和日丽的日子,撸起袖子动手干。
思路
先看看客户端常见的数据埋点方案如下图。
侵入式埋点这个如果在页面少,数据收集需求变化不大的情况下,还是挺经济实惠的。鉴于当前项目页面较多,数据收集的需求变化也在不断的调整,所以侵入式埋点的方案不适用本项目,只有松耦合这个方案了。
小程序的特点
根据松耦合埋点方案,首先要解决的问题是拦截小程序所有控件的操作数据。在Web中有BOM对象及DOM对象可以给我们进行全方位的操作,特别是BOM对象,可以进行全局事件拦截。而小程序类似BOM对象的主要有下面三个对象:App、Page、Component。
- App:App对象有且只有一个,属于小程序的总控对象,App下面包含着n个Page对象。
- Page:Page则通过字面理解就是页面的总控对象,一个小程序可以有多个Page,每一个Page对象对应一个页面,Page下面可以包含n个Component及其他普通的小程序组件。
- Component:Component则是自定义组件对象,类似于Page,只明生命周期及一些特性上有所区别,自定义组件对象在实际项目中比较常用,主要用来解决小程序页面堆栈太小的问题,这个后续再写另外一篇来讲。
小程序的事件机制也是采用与JavaScript一样的事件机制,捕获->执行->冒泡的机制,JavaScript在BOM及DOM对象中都提供了addEventListener这种订阅/发布机制,使我们可以轻松的拦截所有的事件,又不影响现有的流程及代码,从而实现松耦合埋点方案。但是小程序没有提供addEventListener的订阅/发布机制,没有办法通过这样子的方案来实现。
1.拦截器方式
翻了官网,发现没有办法统一的处理,那么只能退而求其次,减少对现有的代码的侵入,于是想到了拦截器的机制来实现。
看看下面小程序原生的代码
App({
onLaunch(options) {
// Do something initial when launch.
},
onShow(options) {
// Do something when show.
},
onHide() {
// Do something when hide.
},
onError(msg) {
console.log(msg)
},
globalData: 'I am global data'
})
提供拦截器,这里只拿App的onLaunch事件进行示例,其他事件都差不多这样处理即可。
var appFilter = function(config){
if(config.onLaunch){
let _onLaunch = config.onLaunch;
config.onLaunch = function(ops){
//在这里干埋点的事
//例如存储数据、上送数据
_onLaunch.call(this);//调用原来的执行逻辑
}
}
return config;
}
//App使用拦截器示例
App(appFilter({
onLaunch(options) {
// Do something initial when launch.
},
onShow(options) {
// Do something when show.
},
onHide() {
// Do something when hide.
},
onError(msg) {
console.log(msg)
},
globalData: 'I am global data'
})
)
这个也是JS好玩的地方,一切皆为对象。通过新增一个Function对象,增强方法之后,把旧的Function对象进行替换。上面的App的onLaunch方法,经过appFilter过滤器的处理后,就替换成过滤器里面加了埋点事件的新方法了,对于开发人员来讲,不需要去改动原来App.onLaunch事件里面的代码,目的达到了,但是不够完美,在写过滤器的实现逻辑时,给了我一个灵感,我是否可以用这种方式来实现不侵入代码的埋点?答案是可以的。
2.增强扩展方式
利用新增一个Function对象,增强方法之后,把旧的Function对象进行替换这样的原理,依次将App、Page、Component这三个对象进行增强扩展,示例代码如下。
//先把原生的三个对象保存起来
const originalApp = App,
originalPage = Page,
originalComponent = Component;
//在原生的事件函数里面,添加数据埋点,并替换成新的事件函数
const _extendsApp = function (conf, method) {
const _o_method = conf[method];
conf[method] = function (ops) {
//在此处进行数据埋点
if (typeof _o_method === 'function') {
_o_method.call(this, ops);
}
}
}
//重新定义App这个对象,将原来的App对象覆盖
App = function(conf){
//定义需要增强的方法
const methods = ['onLaunch', 'onShow', 'onHide', 'onError']
methods.map(function (method) {
_extendsApp(conf, method);
})
//另外增强扩展埋点上送的方法
conf.william = {
addActionData: function (ops) {
console.log('addActionData');
},
addVisitLog: function (ops) {
console.log('addVisitLog');
}
}
return originalApp(conf);
}
//Page及Component对象类似App的处理即可
至此,整个小程序的生命周期都在掌握之中了,可以按需采集对应的数据,并且对于开发人员来讲,还不需要去修改及调整代码,松耦合埋点方案搞定。
3.增强扩展+订阅/发布
上面的实现方案虽然实现了松耦合,但是个人觉得还不够完美,埋点的动作必须要写在增加的方法里面,这样子可维护性较差,也不够灵活。解耦这事现在对我来说信手拈来,祭出了我的EventHub(基于订阅/发布模式实现的消息总线),完美。
//引用EventHub
import EventHub from '../../utils/eventhub.min';
//先把原生的三个对象保存起来
const originalApp = App,
originalPage = Page,
originalComponent = Component;
//在原生的事件函数里面,添加数据埋点,并替换成新的事件函数
const _extendsApp = function (conf, method) {
const _o_method = conf[method];
conf[method] = function (ops) {
//在此处进行数据埋点
//此处改成消息发布
if (typeof EventHub != "undefined") {
EventHub.emit('app' + method, ops);
}
if (typeof _o_method === 'function') {
_o_method.call(this, ops);
}
}
}
//重新定义App这个对象,将原来的App对象覆盖
App = function(conf){
//定义需要增强的方法
const methods = ['onLaunch', 'onShow', 'onHide', 'onError']
methods.map(function (method) {
_extendsApp(conf, method);
})
//另外增强扩展埋点上送的方法
conf.william = {
addActionData: function (ops) {
console.log('addActionData');
},
addVisitLog: function (ops) {
console.log('addVisitLog');
}
}
return originalApp(conf);
}
//Page及Component对象类似App的处理即可
这样子就可以把埋点处理的逻辑抽离到另外一个JS文件中去实现。
EventHub.on('apponLaunch',function(ops){
//在这里可以处理数据埋点的事
})
延展性思考
基于上述的实现方案,我们除了增强生命周期,也可以像上面那样去增加公用的方法,例如App.william.addActionData,更可以通过增强setData方法来进行数据溯源或者进行差量比较来提升性能等方式。