概述
通常一个全新功能上线都会采集用户行为数据,以便数据分析,后续完善优化。而采集数据的行为一般有三个维度:元素点击、元素悬停、元素曝光,下文将研讨如何优雅地采集这三个维度的数据。
方案
原生采集
原生采集顾名思义就是不做任何封装,直接调用第三方数据采集平台提供的SDK
(比如,神策提供的Web JS SDK
)。
const memoRef = useRef({});
const wait = 2000;
const handleClickTrack = () => {
if (memoRef.current.prevClickTime) {
// 已经采集过,无需再次采集
return;
}
if (wait) {
// 频繁点击设置采集时间间隔
if (Date.now() - memoRef.current.prevClickTime >= wait) {
cassSensors.track(eventName, eventData);
memoRef.current.prevClickTime = Date.now();
}
} else {
// 每次都触发采集
cassSensors.track(eventName, eventData);
memoRef.current.prevClickTime = Date.now();
}
}
这种采集方式简单粗暴可靠,理解比较简单,但缺点也比较突出,含目标元素的组件内部都需要实现采集逻辑,对业务代码的侵入性较强,不便于长期迭代维护(比如GrowingIO
替换成神策Web JS SDK
)。
Hooks
Hooks
利用事件冒泡机制精准拦截目标元素采集行为,属于一种切面思想,抽离采集逻辑,降低对业务组件的侵入性。
Document
上述代码中有以下几处需要注意:
- 悬停采集,鼠标从包裹元素移动到子元素时会触发
mouseout
事件,导致采集行为不准确,改用监听mouseleave
事件,只有在鼠标移出包裹元素范围才会触发; - 曝光采集,初始进入页面,此时用户并没有触发滚动事件,当目标元素在可视区域内,或者用户交互过程使得目标元素呈现在可视区域内,该过程也需要采集曝光。
这两种场景的关键在于知道目标元素何时呈现,然后再去判断是否在可视区域。当初次渲染或者更新,React
生命周期precommit
阶段会重新计算绑定在元素上的ref
,当ref.current
有值,就代表元素已经挂载,可采集曝光。 - 如果目标元素既要采集点击、悬停,又要采集曝光,可使用封装好的钩子函数
useTrackable
嵌套包裹
HOC
高阶组件与Hooks
实现思想类似,但是高阶组件需要将目标元素单独抽离成组件包装,相对而言,没有Hooks
简便
Document
插拔式
前端工程的技术栈有很多,Spring MVC、Vue、React
等,如果每种技术栈都去实现一遍,同步维护,成本就比较高了。数据采集都是通过事件机制触发,调用第三方SDK
上报,因此可以简单配置目标元素属性,统一拦截采集事件,封装采集逻辑。
Document
目标元素
有几个特殊场景需要注意:
- 曝光采集,初始页面可视区域的目标元素,和用户交互后呈现出的目标元素应当如何采集?由于此时没有触发滚动事件,
scroll
事件回调捕获不到,可以利用MutationObserver API
监听页面是否有新增DOM
,如果有则执行曝光采集逻辑。IE 11
以下不支持MutationObserver API
,core-js3
并没有实现,可单独引入polyfill
来实现; - 悬停采集方案,一种是使用
mouseenter
与mouseout、mouseleave
组合事件监听悬停;另一种是使用mouseover
监听悬浮,如果悬浮的是目标元素则执行采集逻辑。如果悬浮的不是目标元素则清除所有悬停定时器; 如果一个元素既要点击采集又要悬停采集,嵌套属性配置会导致其中一个事件采集不到,因为
event.target
对应层级高的元素,只拿得到一个属性配置项。解决方法一是改造上述配置规则,可配置多事件属性;二是其中一种事件改成手动采集;目标元素目标元素如果目标元素封装在组件内,属性配置项无法穿透组件直接设置到目标元素上,上报不会触发。解决方法一是使用
window.cassSensorsTrack
手动上报;二是在外层元素添加onMousedownCapture
事件捕获拦截,给组件内目标元素添加属性配置;const Button = () => // 手动采集 const handleClickTrack = () => { cassSensors.track(eventName, eventData); } // onMousedownCapture比onClickCapture优先级高,利用该事件给目标元素配置属性 const handleMousedownIntercept = () => { const $target = document.getElementById('target'); if ($target) { $target.setAttribute('data-eventname', 'CLICK_EVENT'); $target.setAttribute('data-eventtype', 'click'); $target.setAttribute('data-eventdata', '{ id: "click" }'); $target.setAttribute('data-once', 'false'); $target.setAttribute('data-wait', '1000'); } }
注:封装埋点插件不采用冒泡机制,而采用捕获机制的原因是有些业务场景下,事情需要阻止冒泡,可能影响到数据采集
总结
以上三种方案均能抽离封装采集逻辑,优雅实现数据上报,相对而言,第一种
Hooks
实现更为简单,便于理解和维护。第三种插拔式插件封装好处在于可在不同技术栈中使用,但配置项较为复杂。