一、前言
本文代码需结合前文(前文链接: 前端简单埋点sdk的实现思路),前文阐述了前端实现数据采集sdk的一个简单思路,而最近我得到了一个新的需求:采集用户的点击坐标,后续生成热力图,那就这个需求,我也尝试提供一些实现思路,如果大佬们有更好的方法或者发现我的错误,望指导、指正。
二、思路
根据前文,实现了一个数据采集类。而在sdk加载阶段(_sdk_init_loading),在其中添加了点击事件的监听器,但这个简单实现并不能满足我们的需求。我们需要考虑到一下几个情况:
- 单次点击还是双击?
- 如果点击的链接,页面跳转前能否准确记录点击跳转的坐标?
- 事件的触发顺序?
- 事件是否会被阻止?
- 是在pc还是移动点击?点击时是横屏还是竖屏?不同尺寸的设备搜集的坐标如何统一?
一般常用的点击事件有click、dbclick、mousedown、pointdown,而通过实践和经验我们可知道这几个事件的触发顺序为pointdown、mousedown、click、dbclick,并且他们还各有限制,例如mousedown在一些滑动交互的地方可能会被阻止事件,dbclick只在pc端浏览器被触发,在一些a标签点击跳转时,click不会触发…通过了解这些事件的特性,我们可以知道如何才能在不同情况下获取点击坐标。而对于最后搜集的坐标统一性,我们需要根据一个屏幕标准对坐标进行换算。
因此我的实现思路大致整理如下:
- 根据一个设备标准,实现一个坐标换算的方法coordConvert;
- 单独实现点击事件的处理方法clickHandler;
- 初始化点击事件列表clickEventList,将点击事件需要获取的信息全部push到其中;
- 页面卸载前的点击坐标无法通过click事件获取,而mousedown可能又会被banner等滑动性质的元素阻止,所以此处的坐标通过pointdown触发获取并存储。
- 页面卸载前上报点击列表;
三、实现
- 单独实现坐标换算的方法和点击处理的方法
// 构造器初始化需要的变量
constructor(options = {}){
// 点击事件列表
this.clickEventList = [];
// 坐标换算标准尺寸
this.coord_convert_standard = {
pc: {
portrait: { width: 1920, height: 1080 },
landscape: { width: 1920, height: 1080 }
},
tablet: {
portrait: { width: 768, height: 1024 }, // 竖屏
landscape: { width: 1024, height: 768 } // 横屏
},
mobile: {
portrait: { width: 375, height: 667 },
landscape: { width: 667, height: 375 }
}
};
this.pointDownCoord = {};
}
// 获取设备类型
getDeviceType(){
const ua = navigator.userAgent;
if(/Mobile|Android|iP(ad|hone)/.test(ua)){
if(/Tablet|iPad/.test(ua)){
return 'tablet'
}else{
return 'mobile'
}]
}else{
return 'pc'
}
}
// 获取屏幕标准及方向
getScreenStandard = () => {
const device_type = this.getDeviceType();
if (window.matchMedia("(orientation: portrait)").matches) {
return {
direction: 'portrait',
standard: this.coord_convert_standard[device_type]['portrait']
}
} else if (window.matchMedia("(orientation: landscape)").matches) {
return {
direction: 'landscape',
standard: this.coord_convert_standard[device_type]['landscape']
}
} else {
return undefined;
}
};
// 坐标换算的方法
coordConvert(standard, coord) {
if(!standard){
return {
direction: 'unknown',
device_type: this.getDeviceType(),
coord
}
}
const scaleX = window.innerWidth / standard.width;
const scaleY = window.innerHeight / standard.height;
return {
direction: this.getScreenStandard().direction,
device_type: this.getDeviceType(),
coord: [ Math.ceil(coord.x * scaleX), Math.ceil(coord.y * scaleY) ]
}
}
// 点击处理的方法
clickHandler(event, type, coord){
const element = event.target
const clickInfo = {
click_type: type || event.type,
...this.coordConvert(this.getScreenStandard()?.standard ,{ x: coord?.x || event.pageX, y: coord?.y || event.pageY })
}
this.clickEventList.push(clickInfo);
// 清空pointdown存储的坐标,以免影响页面跳转前的坐标的准确性
this.pointDownCoord = {};
}
- 修改sdk加载阶段(_sdk_init_loading)添加点击事件监听器的代码:
// 添加点击事件监听器(放在_sdk_init_loading中)
let _self = this;
const eventList = ['click', 'dblclick', 'pointerdown'];
let clickTimeout = null;
let isDoubleClick = false;
eventList.forEach((event) => {
window.addEventListener(event, (e) => {
switch(event){
case 'click':
// 设置延迟来区分单击和双击事件
clickTimeout = setTimeout(() => {
if (!isDoubleClick) {
_self.clickHandler(e);
}
isDoubleClick = false;
}, 300);
return;
case 'dblclick':
// 设置双击标志并取消单击事件(pc端生效)
isDoubleClick = true;
clearTimeout(clickTimeout);
_self.clickHandler(e);
return;
case 'pointerdown':
// 该监听器主要记录那些会被阻止的点击事件的坐标(页面跳转)
// mousedown事件可能会被swiper等滑动元素阻止,故使用pointdown事件
_self.pointDownCoord = { x: e.pageX, y: e.pageY };
return;
}
});
});
- 页面卸载阶段的改造
_sdk_page_beforeunload(){
let _self = this;
window.addEventListener('beforeunload', function(){
_self.clickHandler(e, undefined, _self.pointDownCoord)
const params = {
click_info_list: _self.clickEventList, // 点击事件列表
}
_self._send_beacon_post('page_unload', params);
})
}