最近项目需要收集用户点击事件并进行操作习惯、模块实用度等分析。在多次调研中发现,网上很多只提供了局部解决思路。后来经过多次尝试、综合了多种方案,最终决定用以下方案实现事件收集并上报存储。(我先一步步利用代码片段带入讲解,全部的代码在最后)
关键词
- H5自定义属性
- React
- 监听
- 全局
- 自定义元素
- 定时上报
开始之前先了解以下两点
- 埋点方案大致分为代码埋点、可视化埋点和全埋点,三种方案的区别和具体介绍可以参考这篇文章,我在实验中使用的全埋点方案,就是页面全局自定义元素监控。
- 全埋点的关键在于H5的自定义属性,关于该属性的介绍可以参考这篇文章。
开始收集
先制造一下业务场景,以下方我的react demo为例,如图,例如我现在在开发网上商城、我要监听用户点击下方商品种类的次数。
步骤1:在要监听的组件中埋入自定义属性
在要监听的组件中埋入自定义属性,先约定自定义属性为data-listened
,属性的值即为所要区分的业务类型,例如手机类的业务类型就是moudule-entry-telephone
,在项目中,最好把属性名放在全局配置中,各个组件统一引用。示例代码:
<div href="#" data-listened="moudule-entry-telephone">手机类</div>
<div href="#" data-listened="moudule-entry-office">办公类</div>
<div href="#" data-listened="moudule-entry-sport">运动类</div>
<div href="#" data-listened="moudule-entry-book">图书类</div>
<div href="#" data-listened="moudule-entry-fashion">时尚类</div>
<div href="#" data-listened="moudule-entry-clothes">穿搭类</div>
步骤2:添加全局监控
在react最高层组建中添加全局监控,一般是在app.js
,添加全局监控一定要在组件加载完成之后监控,即在componentDidMount
生命函数中引用。关键代码:
// 获取到页面所有的被监听的元素,即具有自定义属性“data-listened”的元素
const listenedEles = document.querySelectorAll("[data-listened]") || [];
// 遍历所有被监听的元素,并添加监听事件
listenedEles.forEach(ele => {
// 当被点击后,获取到被点击元素的业务类型,即自定义属性“data-listened”的属性值
ele.onclick
= function () {
// 暂时收集起来,设置定时任务进行上报(提交给服务端)并清空
recordCollecter(ele.getAttribute("data-listened"))
}
})
上报数据
上报方案我采用了先暂存再批量上传的方式,需要处理好暂存变量和上传的先后逻辑。
// 点击事件将会记录在这里
let records = [];
// 点击事件收集器
function recordCollecter(type) {
// 这里上传的列表属性就要根据自己情况定了,暂定一个type和时间戳作为演示
records.push({ type, time: new Date() })
}
// 1、定时上报(5s)
// 2、上报后重新收集
setInterval(() => {
if (records.length > 0) {
setTimeout(() => {
console.log(records);// 1、定时上报,这里要换成上报接口但是我没有开发,暂时打印出来作为演示
records = [];// 2、上报后重新收集,这里在清空的时候不会漏记
}, 0);
}
}, 5000)
基本功能测试
为了验证收集的正确性,我将以每5秒从控制台输出一次暂存的收集数据,并将收集的条数和自己写的全局鼠标监控次数进行对比。(监控鼠标点击次数代码就省略了,react基本技能)
如下图所示,我依次点击了时尚类、图书类和运动类,我的三次点击已经成功收集并从控制台打印出来。
进一步测试
为了验证收集的完整性,我将进行很多次点击,并将打印的数据条数累加起来和鼠标点击的次数进行对比。
如下图所示,我在20秒内点击了66次,控制台分四次打印出来(每5秒打印依次),所得结果13+22+19+22=66
,所以完整性是没问题的。
脱离html文档流的监控问题
我的监控是在html中监控的,那么脱离文档流还可以监控到吗?答案是肯定的,我在另一台电脑上亲测有效,具体就不演示了。
(什么是脱离文档流?参考这里)
代码白给环节
全局组件app.js
:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import userOperateRecorder from "./userClickListener/recorder.js"
class App extends React.Component {
constructor(props) {
super(props);
// 初始化鼠标点击次数
this.state = { count: 0 };
}
componentDidMount() {
// 调用用户操作监控组件
userOperateRecorder()
};
// 鼠标点击计数
mouseCount() {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div className="App" onMouseDown={() => this.mouseCount()} >
<header className="App-header">
{/* 为了验证事件有没有漏记,同时全局监听了鼠标点击事件 */}
<div className="mouse-count">鼠标点击了{this.state.count}次</div>
<div >例如我现在在开发网上商城、我要监听用户点击下方商品种类的次数:</div>
{/* 被监听事件只用定义"data-listened"属性,并指定业务类型,即"data-listened"的值 */}
{/* "data-listened"属性说明:H5支持自定义属性标签,规范都是"data-"作为前缀 */}
<div href="#" data-listened="moudule-entry-telephone">手机类</div>
<div href="#" data-listened="moudule-entry-office">办公类</div>
<div href="#" data-listened="moudule-entry-sport">运动类</div>
<div href="#" data-listened="moudule-entry-book">图书类</div>
<div href="#" data-listened="moudule-entry-fashion">时尚类</div>
<div href="#" data-listened="moudule-entry-clothes">穿搭类</div>
</header>
</div >
);
}
}
export default App;
监控和上报方法recorder.js
:
/*
* @Author: zhaoheng
* @Date: 2020-01-12 17:21:15
* @LastEditTime : 2020-01-12 20:44:06
* @LastEditors : Please set LastEditors
* @Description: 埋点监控用户点击事件
* @FilePath: \react-demo\src\userClickListener\recorder.js
*/
function userOperateRecorder() {
// 获取到页面所有的被监听的元素,即具有自定义属性“data-listened”的元素
const listenedEles = document.querySelectorAll("[data-listened]") || [];
// 遍历所有被监听的元素,并添加监听事件
listenedEles.forEach(ele => {
// 当被点击后,获取到被点击元素的业务类型,即自定义属性“data-listened”的属性值
ele.onclick
= function () {
// 暂时收集起来,设置定时任务进行上报(提交给服务端)并清空
recordCollecter(ele.getAttribute("data-listened"))
}
})
// 点击事件将会记录在这里
let records = [];
// 点击事件收集器
function recordCollecter(type) {
// 这里上传的列表属性就要根据自己情况定了,暂定一个type和时间戳作为演示
records.push({ type, time: new Date() })
}
// 1、定时上报(5s)
// 2、上报后重新收集
setInterval(() => {
if (records.length > 0) {
setTimeout(() => {
console.log(records);// 1、定时上报,这里要换成上报接口但是我没有开发,暂时打印出来作为演示
records = [];// 2、上报后重新收集,这里在清空的时候不会漏记
}, 0);
}
}, 5000)
}
export default userOperateRecorder;