一、前言
前端埋点:通过前端页面上进去埋点,捕获用户行为,提供给数据分析人员。来获取产品的使用情况,进而促进产品的优化和迭代。
二、背景
常见的埋点方案:
手动代码埋点: 精确到用户某个行为进行数据上报。
-
优点:准确,满足产品自定义需求。
-
缺点:代码耦合度太高,不利于代码维护和服用。
全埋点:通过全局捕获事件进行数据上报。
-
优点:能够较大程度支持上报用户数据,降低业务代码耦合度。
-
缺点:对于组件化的前端工程,不能很好兼容。
针对上述描述的常见埋点方案,需要我们提供一种能减低代码耦合度、又能精确上报数据、还能支持组件化的前端工程。
三、埋点方案
结合团队使用的技术栈React、产品运营需求,采用Porps传参的方式实现点击埋点、路由监听实现PV埋点、部分手动埋点来实现埋点需求。
1. Props传参实现点击埋点
initReactTrack({
onClickEvent: (message) => {
console.log(message);
},
});
// Button组件
<Button track={{ a: 1 }}>测试无痕埋点</Button>
// html标签
<div track={{ a: 1 }}>测试无痕埋点</div>
期望值:
initReactTrack函数中的onClickEvent,能够拿到组件或原生Dom中的track数据
实现思路:
jsx语法中的组件、标签是其实是调用的方法是:React.createElement。
结合上述的使用情况,我们只需要重写React.createElement方法即可。只要组件的props中有track和onClick属性,并且track有值,则认为当前元素是需要上报的埋点。
实现代码:
import React from 'react';
import { _history } from 'src/utils/history';
type OnClickEvent = (message:any) => void
type OnRouterChange = (url:string) => void
interface InitReactTrack {
onClickEvent?:OnClickEvent;
onRouterChange?:OnRouterChange;
}
const getReactFCInitializer = ({
onClickEvent,
onRouterChange,
}:InitReactTrack) => {
const originalCreateElement = React.createElement;
const propsWithTrackEvents = function(props:any) {
if (props.track) {
const reactClick = props.onClick;
props.onClick = (e:any) => {
onClickEvent && onClickEvent(props.track);
reactClick && reactClick(e);
};
}
return props;
};
React.createElement = function() {
const args = Array.prototype.slice.call(arguments);
let props = args[1];
if (props && props.track) {
props = propsWithTrackEvents(props || {});
}
return originalCreateElement.apply(null, args);
};
const routerChange = () => {
onRouterChange && onRouterChange(window.location.href);
};
routerChange();
_history.addEventListener(() => {
routerChange();
});
};
export const initReactTrack = ({ onClickEvent, onRouterChange }:InitReactTrack) => getReactFCInitializer({ onClickEvent, onRouterChange });
注意:使用ts开发的项目,给jsx元素添加track属性时会报错,需要手动修改@types/react 的 Attributes ts定义。如下
interface Attributes {
key?: Key | null;
track?:any;
}
2. 路由监听实现PV埋点
initReactTrack({
onClickEvent: (message) => {
console.log(message);
},
onRouterChange: (parmas) => {
console.log(parmas);
},
});
期望值:
当页面路由发生改变时能够在onRouterChange拿到的url路径。
实现思路:
- 前端路由的两种模式:hash、history模式。
- hash路由切换可以通过hashchange去捕获,这里就不再补充。
- history模式,没有相关api去捕获路由切换,所以需要我们自己去实现啊类似hashchange的功能。既:通过发布-订阅模式,重写history的back, forward, go, pushState, replaceState,并添加消息通知,这样一来只要history执行相关方法就能触发发布的事件了。
hissory监听实现代码:
interface Historys {
back():void;
forward():void;
go(delta?:number):void;
pushState(data:any, title:string, url?:string | null):void;
replaceState(data:any, title:string, url?:string | null):void;
}
type Listener = () => void
class HistoryListener{
deeps:Array<Listener> = []
constructor(){
const methods = ['back', 'forward', 'go', 'pushState', 'replaceState'];
methods.forEach((name:keyof Historys) => {
this.registListner(name);
});
}
notify(){
this.deeps.forEach((listener:Listener) => listener());
}
registListner = (name:keyof Historys) => {
const method = history[name];
const _this = this;
history[name] = function(...args:any[]) {
method.apply(history, args);
_this.notify();
};
};
addEventListener(listner:Listener){
this.deeps.push(listner);
window.addEventListener('popstate', listner, false);
}
removeEventListener(listner:Listener){
let i = 0;
while(i < this.deeps.length){
if(this.deeps[i] === listner){
this.deeps.splice(i, 1);
window.removeEventListener('popstate', listner);
break;
}
i++;
}
}
}
export const _history = (() => {
let constancs:HistoryListener;
return () => constancs = constancs ? constancs : new HistoryListener();
})()();
四、小结
通过上述的实现方案,实现了点击埋点、和pv埋点,解决了与业务代码耦合的问题,同时高了埋点效率,只需要在入口引入initReactTrack方法即可。
本文只是对点击埋点和pv的无痕捕获,对于一些具体的业务场景,还需要结合手动埋点来实现。