进入互联网后,数据成爆发式增长,互联网数据分析平台发展如雨后春笋般。这些平台除了提供超级给力的数据分析能力外,还提供了各种数据采集工具,本文就此分享了web前端端数据js采集库开发的心得体验。
有兴趣的朋友可以参考下小伙伴分享的该平台初步介绍:https://blog.csdn.net/qq_28773159/article/details/79752718#10006-weixin-1-52626-6b3bffd01fdde4900130bc5a2751b6d1。
客户端数据采集库是数据分析平台的一个环节,开发sdk时候前,首先明确需求,然后跟数据端和服务端一起确定方案,这里限于篇幅,不再细说。
sdk方案参考如下:
graph TD
A[平台生成APPYKEY] --> B
B[页面接入sdk,填入APPKEY] --> C
C[其它功能性配置后,配置初始化] --> D1
D1[自然来源模块初始化] --> D2
D2[渠道推广初始化] --> D3
D3[jsbridge模块初始化] --> D4
D4[热力图模块初始化] --> D5
D5[热力图模块初始化后,首次进入应用触发da_activate事件] --> D6
D6[是广告推广,触发da_ad_click事件] --> D7
D7[启动单页面功能,修改referrer属性] --> D8
D8[开启一个会话,触发da_session_start事件] --> D9
D9[触发da_screen事件,即PV事件] --> D10
D10[abtest模块初始化] --> E
E[其它自定义事件和设置用户属性流程]
API命名约定:对外API采用下划线分割命名、sdk内部API采用下划线开头驼峰式命名、内部使用的变量下划线开头。
当前只考虑两种:异步接入和同步接入。
A:异步接入
在大部分场景下,我们应尽量保证sdk不影响接入页的渲染,引入js文件时,属性async设置为true ,这样脚本相对于页面的其余部分异步地执行。
在引导js代码中添加:
(function(document, datracker, root) {
// 加载sdk
function loadJs(version) {
var script, firstScript;
script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = 'http://xxx.sdk.'+ sdkVersion +'.js';
firstScript = document.getElementsByTagName('script')[0];
// 将sdk插入到页面所有js脚本的头部
firstScript.parentNode.insertBefore(script, firstScript);
}
// sdkVersion 指定加载的sdk版本
loadJs(sdkVersion);
})(document, window['DATracker'] || [], window);
由于sdk是异步加载,我们必须保证sdk未加载完这段时间内用户能正确调用API。
首先在引导js代码中预注册命名空间和API方法
代码流程
graph TD
A[页面引入导入代码] --> B
B[若window中未注册DATracker,注册] --> C
C[定义init方法] --> C2
C2[注册people类] --> C3
C3[预声明API] --> C4
C4[循环解析声明API,注册API] --> C5
C5[保存配置项]
思考:设计上面这个流程的目的,是为了解决当sdk未加载时,能成功调用API。
那么实现的关键点:
1. 初始化sdk的命名空间;
2. 预定义sdk的API;
3. 将用户调用的API和设置的参数保存到队列中,格式要固定,比如 [ [fn, [arguments]], … ],这样sdk内部才能按照规则解析;
4. 由于还有people类和abtest类,那么设计API的时候,应该类似:DATracker.people.set,DATracker.abtest.get_variation,
这时候解析预定义的API,需要特别处理;
5. 若用户引用的引导代码和实际加载的sdk版本有兼容问题,那么应该在引导代码增加版本编号,
同时在sdk加载完毕后,做对应的判断,若不支持,应抛出提示,阻止继续进行;
6. sdk的凭证和配置信息保存到变量,当sdk加载完毕后,实例化需要用到;
思路其实已经很清晰了,首先window下注册sdk命名空间,类型为数组,然后再注册API方法,用户每次调用API,就把fn和参数保存到队列中。
等到sdk加载完毕后,再实例化sdk,同时将window下预注册的sdk命名空间指向实例化的sdk。
实现如下:
(function(document, datracker, root) {
// ...此处省略加载sdk代码
// 注册sdk对象
// 首先判断sdk是否已初始化过,可通过判断是否有某个属性,如版本
if (!datracker['sdkVersion']) {
// 注册sdk命名空间
window['DATracker'] = datracker;
datracker['libraryConfig'] = {
token: '',
config: {}
};
// 引导代码版本,其实就是sdk对应的大版本啦
datracker['sdkVersion'] = 'xx.xx';
// 设置方法
function _setAndDefer(target, fn) {
var split = fn.split('.');
// 若数组长度等于2,说明是people类
if (split.length === 2) {
target = target[split[0]];
fn = split[1];
}
// 这里将用户调用的方法以及设置的参数保存到队列汇总
// 当sdk加载完毕后,将执行队列中方法
// 保存格式:[ [fn, [arguments]] ]
target[fn] = function() {
target.push([fn]
.concat(Array.proptotype.slice.call(arguments, 0)));
};
}
// datracker对象注册sdk init方法
// token: 接入数据平台的凭证
// config: sdk配置信息
/**
// 调用 register_attributes:
DATracker.init('appkey-xxx', {});
DATracker.register_attributes({'event': 'test-page'});
*/
datracker['init'] = function(token, config) {
datracker['people'] = datracker['people'] || [];
datracker['abtest'] = datracker['abtest'] || [];
// 预声明API,空格分割,示例
var functions = 'register_attributes register_attributes_once clear_attributes people.set abtest.get_variation'.join(' ');
for (var i = 0; i < functions.length; i += 1) {
_setAndDefer(datracker, functions[i]);
}
// 保存配置
datracker['libraryConfig'].token = token;
datracker['libraryConfig'].config = config;
};
}
})(document, window['DATracker'] || [], window);
功能说明:我们是用户行为分析平台,所以sdk提供了设置事件和设置用户两大类API。
当用户使用异步方式时,首先将上面的代码引入,然后调用 DATracker.init(‘xxx’) 初始化sdk。
等待sdk加载完毕后,初始化sdk,从队列中依次真正执行API方法。
思路:
1. 首先我们应该声明sdk库类和people类,同时分别扩展 _init方法;
2. 实现sdk实例化方法,实例化后,运行队列中的方法,返回实例化对象;
3. sdk加载完毕后,调用入口函数init_async,保证只实例化一次,实例化成功后,将window下预定义的sdk命名空间指向实例化;
实现如下:
// 引入纯函数帮助类文件,
// 不涉及sdk业务
// 不再展开细说
import _ from './utils';
//引入abtest
import abtest from './abtest';
// 引入sdk方式类型:同步方式标记
var INIT_SYNC = 1;
// 引入sdk方式类型:异步方式标记
var INIT_ASYNC = 2;
// 当前引入sdk方式类型标记
var _initType;
// 保存实例的变量
var _datrackerMaster;
// sdk库类
var DATrackerLib = function() {};
// sdk People 类
var DATrackerPeople = function() {};
// sdk库内部初始化方法
DATrackerLib.prototype._init = function(token, config) {
var _self = this;
_self['_hasLoaded'] = true;
//.... 此处暂时省略
};
// sdk People 类初始化方法
DATrackerPeople.prototype._init = function(_DATrackerInstance) {
this._DATracker = _DATrackerInstance;
};
// 执行队列中的方法
// 队列中item格式:[ [fn, [arguments]] ]
DATrackerLib.prototype._executeArray = function(array) {
_.each(array, function(item) {
if (item) {
// 要保证item保存的是实例中的方法
if (_.isArray[item] && _.isFunction(this[item[0]])) {
// 执行方法
this[item[0]].apply(this, item.slice(1));
}
}
}, this);
};
// 实例化sdk
var _createDtlib = function(token, config) {
var instance;
var target = _datrackerMaster;
// 如果引入sdk方式是异步
if (target && _initType === INIT_ASYNC) {
// 检测 ,是否已初始化
// 即检测_datrackerMaster
if (!_.isArray(target)) {
// console.log('已经实例化了');
return;
}
}
// 调用库对象
instance = new DATrackerLib();
// sdk库调用 _init方法,
// 各个子模块初始化
instance._init(token, config);
// 设置People类
instance['people'] = new DATrackerPeople();
instance['people']._init();
instance['abtest'] = abtest;
instance['abtest']._init(instance);
// 接下来执行队列中的方法,
// 只要判断 target 类型是否为数组
// 因为若是异步引入的话,
// window['DATracker']一开始定义的类型为数组
if (!_.isUndefined(target) && _.isArray(target)) {
// 执行people类的队列
instance._executeArray.call(instance['people'], target['people']);
// 执行abtest类的队列
instance._execute_array.call(instance['abtest'], target['abtest']);
// 执行其它方法
instance._executeArray.call(target);
}
return instance;
};
// 为啥要写到这个函数中,因为我们还要支持其他加载方式呀
// 打包的时候,只要引入不同的入口函数
export function init_async() {
// 获取window下注册的sdk对象
_datrackerMaster = window['DATracker'];
// 判断是否注册
if (_.isUndefined(_datrackerMaster) ) {
return;
}
// 同样的,我们要判断是否初始化过
if (_datrackerMaster['_hasLoaded']) {
return;
}
// 设置当前引入sdk方式类型标记为异步
_initType = INIT_ASYNC;
// 当然,如果使用的引导代码版本过低(引入的sdk版本太低了),
// 我们得阻止执行啦
var version = _datrackerMaster['sdkVersion'] || 0;
// 当前引导sdk版本不支持已下载的sdk
if (version < 2.0) {
console.log('引导代码版本太低啦,更新下吧');
return;
}
// 实例化sdk类的各个子模块
_datrackerMaster = _createDtlib.
apply(this, _datrackerMaster['libraryConfig']);
// 修改DATracker对象,重新指向的才是sdk实例
window['DATracker'] = _datrackerMaster;
}
B:同步接入
目标:sdk加载完毕后,能立即调用API执行。
首先:页面上需直接引入sdk文件:
<script src="xxx.xx.js"></scirpt>
其次:加载完毕后,调用入口函数init_sync ,实例化sdk,同时在window下注册sdk命名空间,指向该实例;
实现如下:
export function init_sync() {
// 设置当前引入sdk方式类型标记为同步
_initType = INIT_SYNC;
// 实例化sdk类以及各个子模块
_datrackerMaster = _createDtlib.
apply(this, _datrackerMaster['libraryConfig']);
// window下注册sdk对象,指向sdk实例
window['DATracker'] = _datrackerMaster;
}
sdk内部初始化设计
我们从sdk初始化调用的_init方法开始讲起。
思考下,init方法中我们需要加什么?
我们回到准备阶段目录,里面讲解了sdk中有N个子模块,这些子模块什么时候初始化在这里探讨下。
从网站自然来源讲起:
当从百度跳转到站点后,sdk此时初始化完毕,上报了各种事件数据,对此我们要记住用户是从百度过来的并保存到本地,同时每次上报事件都需要加上该数据。自然每次上报数据前,我们可以获取下该数据。但是该模块并非简单简单的获取referrer属性,首先我们要判断是否外链,若是得更新本地数据,进入站点其它页面后,我们需要再次从本地中获取数据。我们在设计该模块也需要有个 init 方法,自然该模块初始化最好是在 sdk的 _init方法内,而且必须最开始执行。
import _ from './utils';
import source from './source';
// ....省略
// sdk库内部初始化方法
DATrackerLib.prototype._init = function(token, config) {
var _self = this;
_self['_hasLoaded'] = true;
source.init(token);
};
站点来源还有一个模块就是渠道推广了,原理跟自然来源模块其实是一样,不同的是判断依据和属性值。同样,也是必须最开始执行初始化。
import _ from './utils';
import source from './source';
import campaign from './campaign';
// ....省略
// sdk库内部初始化方法
DATrackerLib.prototype._init = function(token, config) {
var _self = this;
_self['_hasLoaded'] = true;
source.init(token);
campaign.init(token);
};
jsbridge模块的一个作用是打通h5和APP之间的数据互相传输,自然我们期望能马上初始化。
import _ from './utils';
import source from './source';
import campaign from './campaign';
import appJsBridge from './appJsBridge';
// ....省略
// sdk库内部初始化方法
DATrackerLib.prototype._init = function(token, config) {
var _self = this;
_self['_hasLoaded'] = true;
source.init(token);
campaign.init(token);
//打通app与h5
var bridegObj = app_js_bridge();
//获取native传递给客户端数据
_self.get_appStatus = function(func) {
var callback = function(app_info) {
try{
if(typeof func === 'function') {
func(app_info);
}
} catch(e){}
};
bridegObj.getAppStatus(callback);
};
//发送数据到native
_self._get_SendCall = function(data, event_name, callback, jsTrack) {
if(_self.get_config('is_heatmap_render_mode')) {
return false;
}
bridegObj.getSendCall(data, event_name, callback, jsTrack);
};
};
后面我们会讲解该模块的实现,接下来就是热力图模块了。
热力图模块有两大功能:
1. 自动采集目标元素的数据;
2. 拉取数据,绘制热力图;
当sdk进入展示热力图模式时,为了不影响数据的正确性,页面触发的任何事件我们都不能上报,所以我们就有了 is_heatmap_render_mode
这个字段做标记。也因为这个功能需求,剩余子模块必须在热力图模块初始化完毕后,才能确定是否初始化。这些子模块一旦初始化,都会触发指定事件。
// ...省略
import heatmap from './heatmap';
// ...省略
DATrackerLib.prototype._init = function(token, config) {
var _self = this;
// ...省略
heatmap.init(_self, function() {
_self._loaded();
_self._send_da_activate();
// 发送广告点击事件
if(campaign.data.isAdClick) {
_self._ad_click();
}
// 启动单页面,修改 referrer
if(_self.get_config('is_single_page')) {
_self['cookie'].register({
currentReferrer: location.href
});
// 单页面模块初始化
_self._single_page();
}
// 启动自动触发 PV 事件
if (_self.get_config('track_pageview')) {
_self.track_pageview();
} else {
// session模块检测
_self._session();
}
});
}
自此,在_init 方法里添加了各子模块的初始化过程。
在 _init方法里,我们应用了_getConfig、_register、_registerOnce、_setConfig
、track_
类的这些方法,我们来分析下。
设计sdk的时候,我们考虑了各子模块的设计,同时通过一些规则,保证子模块和主模块之间、子模块和子模块之间和谐相处。这些规则是系统配置、临界点、依赖条件、上报数据、数据传递等,我们需要设计一些必须的API来操作规则。
方法 | 说明 |
---|---|
_setConfig() | 设置实例配置信息 |
_getConfig() | 获取实例配置信息 |
_register() | 保存设置的属性值 |
_registerOnce() | 只设置保存一次 |
track() | 处理设置事件信息并上报事件信息 |
people.set() | 处理设置用户信息并上报用户属性 |
_sendRequest() | 发送数据方法 |
当然还有其它一些API,这类API中,有些必须在设计sdk时考虑进去,有些是开发时候考虑。
发送数据API设计
下面我们就分析 track 这个API,该API即给内部使用,也给用户调用,是上报数据的核心API。
它应该包含哪些功能?
设计该API,它核心功能确定为处理事件数据并上报数据。作为数据采集sdk,这是必须有的API。
基于这个设计思想,一开始应该这样:
// 事件跟踪
DATrackerLib.prototype.track = function(eventName, properties) {
if (_.isUndefined(eventName)) {
return;
}
this._sendRequest(eventName, properties);
}
在讲解热力图的时候,已经知道当 is_heatmap_render_mode
标志值是true时,不能发送数据:
// 事件跟踪
DATrackerLib.prototype.track = function(eventName, properties) {
if (_.isUndefined(eventName)) {
return;
}
// 热力图渲染模式不上报数据
if (this._getConfig('is_heatmap_render_mode')) {
return;
}
this._sendRequest(eventName, properties);
}
基于模型设计,上报的任意事件都应带有公共属性:
// 事件跟踪
DATrackerLib.prototype.track = function(eventName, properties) {
if (_.isUndefined(eventName)) {
return;
}
// 热力图渲染模式不上报数据
if (this._getConfig('is_heatmap_render_mode')) {
return;
}
var data = {
// ...此处省略
attributes: {}
};
// 渠道推广信息
data = _.extend(data, campaign.getParams());
// 自然流量来源信息
data = _.extend(data, source.getParams());
// 自定义属性
data.attributes = properties || {};
this._sendRequest(eventName, data);
}
由于数据模型中有costTime字段 ,即用户去触发某个事件的耗时,比如:监听用户进入页面后,去点击播放按钮花费的时间。
那么需要判断timeEvent队列中是否有当前触发的事件,有的话设置costTime值,并清除掉。
// 事件跟踪
DATrackerLib.prototype.track = function(eventName, properties) {
// ...此处省略
// timeEvent 队列中,eventName注册的时间
var startTimesTamp;
// 耗时
var costTime;
if (!_.isUndefined( this['storage'].props.costTime[eventName] )) {
delete this['storage'].props.costTime.eventName;
this._register(this['storage'].props);
}
if (!_.isUndefined(startTimesTamp)) {
costTime = new Date().getTime() - startTimesTamp;
}
var data = {
// ...此处省略
costTime: costTime
attributes: {}
};
// ...此处省略
this._sendRequest(eventName, data);
}
会话模块
接下来就是session了,这块得单独讲解下。
这里所谓的session,指的是这段时间内,用户的行为轨迹。我们设计的时候,考虑两点:
1. 设计会话开始和会话结束;
2. 保证所有用户行为事件都在某个会话内;
先从第二点讲起:
我们设想下,用户触发一个行为后,sdk得给它指定到当前的会话内,最简单的方式就是在track里实现这个指定。
首先用户触发了一个行为,sdk执行track方法,此时我们认为该行为属于当前会话,同时我们开启会话检测。
// 事件跟踪
DATrackerLib.prototype.track = function(eventName, properties) {
// ...此处省略
// 当触发的事件非指定的这些事件(da_session_start,da_session_close,da_activate),
// 触发检测 _session() 方法,如果过期,就新启一个会话
if (!_.include(['da_session_start', 'da_session_close', 'da_activate'], eventName)) {
this._session();
}
// ...此处省略
}
我们已经假设当前已经有个会话,我们回到讲解 _init方法,可以看到已经做session检测了。
再来说第一点:
首先我们确定一个时间间隔,然后再判断是否关闭当前会话,开启新会话,具体实现放置在 _session方法内。
会话开始有三种情况:
1. 当前无会话;
2. 最近一次触发的用户行为事件超出当前会话时间;
3. 从第三方网站跳转过来;
我们开始设计第一种情况:当前无会话。
无会话,也可以理解为会话开始时间为0,故在 _init 方法里,我们首先注册会话时间,并设置为0:
// ...省略
DATrackerLib.prototype._init = function(token, config) {
_self = this;
// ...省略
_self['storage']._registerOnce({
sessionStartTime: 0
});
// ...省略
};
此时进入session判断:
DATrackerLib.prototype._session = function() {
var sessionStartTime = this['storage'].props.sessionStartTime / 1000;
if (sessionStartTime === 0) {
this['storage']._register({
sessionUuid: _.UUID(),
sessionStartTime: new Date().getTime()
});
this.track('da_session_start');
}
};
从第三方网站跳转过来,原先的会话结束,开启新会话。通过referrer来判断是否第三方跳转过来。
DATrackerLib.prototype._session = function() {
var sessionStartTime = this['storage'].props.sessionStartTime / 1000;
//其它渠道
var otherWBool = !this._check_referer();
var set = function() {
this['storage']._register({
sessionUuid: _.UUID(),
sessionStartTime: new Date().getTime()
});
};
if (sessionStartTime === 0) {
set();
this.track('da_session_start');
}
if (otherWBool) {
// 关于 session close,需要单独处理,这里只是展示用
this.track('da_session_close');
set();
this.track('da_session_start');
}
};
会话时间是否超期,首先需要设计一个超时时间,业内一般设计为30分钟;第二需要确定最近行为事件触发时间,然后跟当前时间作比较,这个时间我们可以每次触发非session事件后,保存下来。
首先获取最近触发的时间,再跟当前时间做比较,超过设定范围,开启新会话。
DATrackerLib.prototype._session = function() {
// ...省略
var updatedTime = this['storage'].props.updatedTime / 1000;
// 获取当前时间
var nowDateTime = new Date().getTime();
if ( nowDateTime/1000 > updatedTime + 60 * this._getConfig('session_interval_mins') ) {
// 关于 session close,需要单独处理,这里只是展示用
this.track('da_session_close');
set();
this.track('da_session_start');
}
};
至于最近行为触发时间,因为每次触发 track 时,若非session事件,我们都会调用 _session方法,所以设计的时候,就直接在 _session中保存。
DATrackerLib.prototype._session = function() {
// ...省略
var updatedTime = this['storage'].props.updatedTime / 1000;
// 获取当前时间
var nowDateTime = new Date().getTime();
if ( nowDateTime/1000 > updatedTime + 60 * this._getConfig('session_interval_mins') ) {
// 关于 session close,需要单独处理,这里只是展示用
this.track('da_session_close');
set();
this.track('da_session_start');
}
this['storage']._register({
updatedTime: nowDateTime
});
};
这里还要再说明一下,所谓 session close,指的是一段时间内,若没有触发任何行为事件,那么我们就认为该会话已结束。 故可以看到上面代码,每次track时,我们就进入 _session方法内,同时更新 updatedTime。
接下来再来分析下 session close:
每个事件模型都有个time属性,这个属性表示事件的触发时间,事件触发时间一般来说都是当前时间,那么close事件的time属性应该也是当前时间了。但是我们之前讲过,每个session时间段是固定的,也就是说触发close时,它的time不应该超过设定的超时时间,否则查看用户行为轨迹就错乱了。所以close的time属性应该是最后一次触发的非session事件的时间才对。
首先我们在 track API中保存非session事件的触发时间,同时session事件的time要特殊设置
DATrackerLib.prototype.track = function(eventName, properties) {
// ...省略
// 当前时间
var time = new Date().getTime();
// 事件为 session时,重新设置 time
if (eventName == 'da_session_close') {
time = this['storage'].props.sessionCloseTime;
}
if (eventName == 'da_session_start') {
time = this['storage'].props.sessionStartTime;
}
// 保存非session事件的触发时间
if( !_.include(['da_session_close','da_session_start'], eventName) ) {
this['storage']._register({
LASTEVENT: {
eventId: eventName,
time: time
}
});
}
};
然后我们新设计了一个方法,用来处理 session close事件,该事件还有一个 sessionTotalLength 属性,表示当触发close时,该会话持续时长。
我们已经说过,session是表示一段时间内,用户的行为轨迹集合,那么close事件的time 设计的时候,应该考虑不能跟该会话的最后一次行为事件time一样,否则的话行为轨迹就混乱了。
DATrackerLib.prototype._trackDaSessionClose = function() {
var sessionStartTime = this['storage'].props.sessionStartTime;
// 当前时间减去1,是为了不跟下一个会话的事件time一致
var time = new Date().getTime() - 1;
// 如果本地保存了该会话最后一次的行为事件time,重新设置 time
var LASTEVENT = this['storage'].props.LASTEVENT;
if (LASTEVENT && LASTEVENT.time) {
// 不能跟最后一次行为事件time一样,得加1
time = LASTEVENT.time + 1;
}
var sessionTotalLength = time - sessionStartTime;
this.track('da_session_close', {
sessionCloseTime: time,
sessionTotalLength: sessionTotalLength
});
};
pv设计
pv是网页统计分析基本的指标,设计采集该指标时需考虑sdk自动上报。
一般情况下,用户进入网页后,就只触发一次PV,故只要在sdk初始化阶段上报即可。
// ...省略
DATrackerLib.prototype._init = function(token, config) {
var _self = this;
// ...省略
heatmap.init(_self, function() {
self.track('da_screen');
});
};
// ...省略
PV事件的自定义属性设计
我们事件模型中,任意行为事件是可以自定义属性的,上面实现不能满足用户也能设置PV事件的自定义属性。由于PV事件是sdk内部触发的,我们需要提供给用户一个钩子函数。当然设计该钩子函数,不应该仅仅是满足这个功能,否则就得不偿失了。
首先,我们配置项里添加钩子配置,当sdk初始化时生效该配置。
var DEFAULT_CONFIG = {
// ...省略
'loaded': function() {}
};
然后在 _init 方法内执行该配置
// ...省略
DATrackerLib.prototype._init = function(token, config) {
// ...省略
heatmap.init(_self, function() {
self._getConfig('loaded')(self);
self.track('da_screen');
});
// ...省略
};
设置一个变量,保存用户设置PV的自定义属性,触发 PV 时,使用该属性。
首先页面上配置钩子函数,然后设置 pageview_attributes
//sdk初始化,执行该方法
var beforeFn = function(datracker) {
//datracker 为sdk实例对象
//pageview_attributes, pageview事件的自定义事件属性
datracker.pageview_attributes = {
test: '12344'
};
};
DATracker.init('xxxxx',{ loaded: beforeFn});
track方法内,判断若是pv事件,添加自定义属性 pageview_attributes 。
// 常量,内置事件列表
var DEFAULTEVENTID = {
// ...省略
'da_screen': {
'dataType': 'pv'
},
// ...省略
};
// 事件跟踪上报
DATrackerLib.prototype.track = function(eventName, properties) {
// ...省略
// 事件类型,默认值 'e'
var dataType = 'e';
if (DEFAULTEVENTID[eventName]) {
dataType = DEFAULTEVENTID[event_name].dataType;
}
//触发pageview时,外部设置了该pv的自定义事件属性,这里发送该自定义事件属性
if(dataType === 'pv') {
if(typeof this.pageview_attributes === 'object') {
userSetProperties =
_.extend({}, this.pageview_attributes || {}, userSetProperties);
}
}
// ...省略
}
单页面模式设计
上面已经讲过,用户每次访问页面时,sdk初始化,然后就会自动上报PV事件。但是如果站点应用是单页面类型时,由于sdk只初始化一次,此时PV事件只发送一次,显然,这是不合理的。
现有的单页面技术,一般有三种: hash、history、memoryhistory。 至于memoryhistory模式,一个应用一套方案,我们并没有合适的手段添加PV变动监听,故我们希望用户方自己手动调用下面 API触发PV事件上报。
DATrackerLib.prototype.track_pageview = function(attributes, page, call) {
if (_.isUndefined(page)) {
page = document.location.href;
}
var self = this;
var callback = function() {
var data = self.track('da_screen', _.extend({}, attributes));
if(typeof call === 'function') {
call(data);
}
};
self._session(callback);
};
自动监听PV事件设计
默认sdk是不应该启动PV事件监听的,因为我们无法判断应用是否采用单页面技术方案。所以我们需要设计配置项,让用户来启动单页面应用PV监听,同时让用户确定是 hash
还是 history
方案实现。
页面上调用初始化方法,开启单页面应用,采用 hash 模式:
DATracker.init('xxxxx', {
is_single_page: true,
single_page_config: {
mode: 'hash' // mode: 'history'
}
});
is_single_page 字段就是启动单页面应用开关了, mode 字段是单页面应用技术方案。
设计好用户使用方式后,接下来就是实现PV监听了。
当mode是 ‘hash’时,实现的原理是监听浏览器的’hashchange’事件;
当mode是 ‘history’时,实现的原理是监听浏览器的 ‘popstate’ 事件,以及监听调用浏览器的 ‘pushState’ 和 ‘replaceState’ API;
首先我们设计singlePage类
var singlePage = {
config: {
mode: 'hash',
callback_fn: function() {}
},
init: function(config) {
this.config = _.extend(this.config, config || {});
this._onEvent();
},
_onEvent: function() {
if (this.config.mode === 'hash') {
_.register_event(window, 'hashchange', _.bind(this._handleHashState, this));
}
},
_handleHashState: function() {
this._handleUrlChange();
},
_handleUrlChange: function() {
var _self = this;
// 等页面url变动后,再执行 callback_fn
setTimeout(function() {
if(_self.config.mode === 'hash') {
if(typeof _self.config.callback_fn === 'function') {
_self.config.callback_fn.call();
}
}
}, 0);
}
};
上面我们设计了singlePage类,实现了 ‘hash’ 的监听, 配置项中 callback_fn 是监听变动后的回调函数。
接下来我们实现 history 的监听。
我们知道,history 变动的时候,将触发 popstate 事件。 用 history 技术实现单页面,切换页面的时候,一般调用 pushState
或者 replaceState
,但是调用这两个API,不会触发popstate事件,所以得重写这两个API,以便监听是否调用。 还有一点需要注意,调用 replaceState
方法后触发的变动,我们默认是不算PV的,但我们也需要设计开关,让用户配置。
var singlePage = {
config: {
mode: 'hash',
track_replace_state:false,
callback_fn: function() {}
},
init: function(config) {
this.config = _.extend(this.config, config || {});
this.path = _getPath();
this._onEvent();
},
_onEvent: function() {
// ...省略
if (this.config.mode === 'history') {
if (!history.pushState || !window.addEventListener) return;
this._on(history, 'pushState', _.bind(this._pushStateOverride, this) );
this._on(history, 'replaceState', _.bind(this._replaceStateOverride, this) );
window.addEventListener('popstate', _.bind(this._handlePopState, this));
}
},
_pushStateOverride: function() {
this._handleUrlChange(true);
},
_replaceStateOverride: function() {
this._handleUrlChange(false);
},
_handlePopState: function() {
this._handleUrlChange(true);
},
// ...省略
_handleUrlChange: function(historyDidUpdate) {
var _self = this;
// 等页面url变动后,再执行 callback_fn
setTimeout(function() {
// ...省略
if (self.config.mode === 'history') {
if(historyDidUpdate || self.config.track_replace_state) {
if(typeof self.config.callback_fn === 'function') {
self.config.callback_fn.call();
}
}
}
}, 0);
},
_getPath: function() {
return location.pathname + location.search;
},
_on: function(obj, event, callFn) {
if(obj[event]) {
var fn = obj[event];
obj[event] = function() {
var args = Array.prototype.slice.call(arguments);
callFn.apply(this, args);
fn.apply(this, args);
};
} else {
obj[event] = function() {
var args = Array.prototype.slice.call(arguments);
callFn.apply(this, args);
};
}
}
};
上面我们基本实现了history的监听,但有几个问题需要解决:
1. 我们通过重写API方式来实现监听,如果用户设置了两次相同的url,此时PV就重复发了;
2. 如果设置的url为空,此时PV还是会触发,这是不合理的;
优化后的实现:
var singlePage = {
// ...省略
_handleUrlChange: function(historyDidUpdate) {
var _self = this;
// 等页面url变动后,再执行 callback_fn
setTimeout(function() {
// ...省略
if (self.config.mode === 'history') {
var oldPath = self.path;
var newPath = _getPath();
// url不相同且都不为空
if(oldPath != newPath && self._shouldTrackUrlChange(newPath, oldPath)) {
self.path = newPath;
if(historyDidUpdate || self.config.track_replace_state) {
if(typeof self.config.callback_fn === 'function') {
self.config.callback_fn.call();
}
}
}
}
}, 0);
},
// url存在
_shouldTrackUrlChange: function(newPath, oldPath) {
return !!(newPath && oldPath);
}
// ...省略
};
singlePage类我们已经设计完毕。
下面是调用singlePage类的方法:
// 单页面应用初始化调用方法
DATrackerLib.prototype._singlePage = function() {
var _self = this;
var fn = function() {};
if(_self.get_config('single_page_config').mode === 'hash') {
fn = function() {
_self._singlePageview();
};
} else if(_self.get_config('single_page_config').mode === 'history') {
fn = function() {
_self._singlePageview();
};
}
single_page.init({
mode: _self._getConfig('single_page_config').mode,
track_replace_state: _self._getConfig('single_page_config').track_replace_state,
callback_fn: fn
});
};
在数据模型中,每一个行为事件都带有 referrer 属性,这个属性我们是通过 document.referrer
拿的,但是单页面技术开发的应用,切换页面时,我们拿到的属性值是错误的,比如都是空字符串。这时候我们需要设计一个方案,保存上一个页面的url作为 referrer 属性值。
// ...省略
DATrackerLib.prototype._init = function(token, config) {
// ...省略
_self['storage']._register({
sessionReferrer: document.referrer
});
// ...省略
};
//发送单页面的PV事件
DATrackerLib.prototype._singlePageview = function() {
// ...此处省略
};
// 事件跟踪
DATrackerLib.prototype.track = function(eventName, properties) {
// ...此处省略
if (dataType === 'e') {
}
//启动单页面
//解决单页面切换时无 referrer 问题
if(this.get_config('is_single_page')) {
if(properties['sessionReferrer'] != properties['referrer']) {
properties['referrer'] = properties['sessionReferrer'];
}
}
}
敬请期待……