规律是创造与发现的,记忆是规律的推导与提取
埋点系统
首先我们考虑到业务需求的H5数据一般为PVUV和页面的点击事件,那么我们的埋点系统就需要对页面信息进行收集,同时对所有的点击事件进行收集。我们简单设计一下数据结构:基础信息+事件+属性。 基础信息主要是页面信息我们可以通过ua和location获取:
export const GetPageInfo = () => {
const url = window.location.href;
const hash = window.location.hash;
const referrer = document.referrer;
const title = document.title;
const query = window.location.search;
const host = window.location.host;
const pathname = window.location.pathname;
const protocol = window.location.protocol.split(":")[0];
const port = window.location.port;
return {
url,
host,
pathname,
protocol,
port,
hash,
referrer,
title,
query: query.substring(1),
};
};
data: Record<string, any> = {
version: "1.0.0",
distinct_id: UUID(),
event: "",
env: navigator.userAgent,
time: new Date().getTime(),
...GetPageInfo()
};
上报前需要转化一下数据格式:
const { version, distinct_id, event } = this.data;
const data = {
version,
distinct_id,
event,
properties: {
...Object.keys(this.data).reduce((pre, cur) => {
if (["version", "distinct_id", "event"].indexOf(cur) === -1) {
pre[cur] = this.data[cur];
}
return pre;
}, {} as Record<string, any>),
},
};
数据结构设计好了之后,需要考虑作为开发人员我们需要在什么时候收集数据呢,对于PVUV肯定是页面第一次加载和路由切换变化的时候进行上报,我们需要对页面加载和路由切换事件进行监听:
setPageLoadListener() {
const trackPage = this.trackPage.bind(this);
// 监听页面加载
document.addEventListener("DOMContentLoaded", function () {
trackPage("pageload");
});
// 监听页面跳转
window.addEventListener("hashchange", function () {
trackPage("hashchange");
});
// 监听页面前进后退
window.addEventListener("popstate", function () {
trackPage("pageview");
});
let historyPush = window.history.pushState;
window.history.pushState = function () {
historyPush.apply(window.history, arguments as any);
trackPage("pageview");
};
}
对于点击事件,我们可以约定自动上报只针对a标签和button标签的点击事件进行监听,而手动上报可以针对任意html元素。
点击事件业务那边需要的信息一般为元素标识即事件属性,我们可以约定以className为默认属性,当然对于开发人员定位问题我们可以获取该元素选择器路径(主要是为可视化埋点预留扩展接口)。
这里我们主要定义自动上报和手动上报的方法。
自动上报我们通过监听document上的click方法,通过对事件冒泡的拦截来获取选择器路径:
checkValidTagName(tagName: string) {
const validTagNames = ["a", "button"];
return validTagNames.indexOf(tagName.toLocaleLowerCase()) > -1;
}
// 以body或者其下第一个容器元素为开始节点,生成selector path
getElementPathName(el: HTMLElement) {
if (el.id && el.id !== "") {
return `#${el.id}`;
} else {
const parentElement = el.parentElement as HTMLElement;
const childIndex = Array.prototype.indexOf.call(
parentElement.children,
el
);
if (
el.parentNode &&
parentElement.tagName.toLocaleLowerCase() === "body"
) {
return `body > ${el.tagName.toLocaleLowerCase()}:nth-child(${
childIndex + 1
})`;
} else {
return ` > ${el.tagName.toLocaleLowerCase()}:nth-child(${
childIndex + 1
})`;
}
}
}
// 判断是否是有效自动上报节点 同时生成上报数据
getClickElementInfo(ev: MouseEvent) {
// 重置点击数据
let validClick = false;
this.clickData = {};
const pathEls = ev.composedPath() as HTMLElement[];
let elementInPath = [] as HTMLElement[];
let hasValidElement = false;
let targetElement;
// 从下往上查找有效的点击元素,a标签或者button内可能包含其他元素
while (pathEls.length > 0) {
const el = pathEls.shift() as HTMLElement;
if (el.tagName && this.checkValidTagName(el.tagName)) {
hasValidElement = true;
targetElement = el;
break;
}
}
elementInPath = pathEls.reverse().slice(4);
elementInPath.push(targetElement as HTMLElement);
if (elementInPath.length > 0 && hasValidElement) {
const target = elementInPath[elementInPath.length - 1];
const tagName = target.tagName.toLowerCase();
const className = target.className;
const content = target.innerText;
// selector拼接 保证querySelector能正确定位元素
const selector = elementInPath
.map((item) => this.getElementPathName(item))
.join("");
this.clickData = {
event: "click",
action: "click",
selector,
content,
className,
tagName,
};
if (tagName === "a") {
const href = target.getAttribute("href");
this.clickData.targetUrl = href;
}
validClick = true;
}
return validClick;
}
手动上报比较简单,我们根据当前传入元素的父元素进行遍历就行:
getPathElemsByEl(el: HTMLElement) {
let elementInPath = [] as HTMLElement[];
while (el.parentNode && el.tagName.toLocaleLowerCase() !== "body") {
elementInPath.push(el);
el = el.parentNode as HTMLElement;
}
return elementInPath.reverse();
}
根据这两个方法我们再封装监听方法和手动上报方法:
// 监听方法 全局自动上报
setElementListener() {
document.addEventListener("click", (e) => {
this.getClickElementInfo(e) && this.trackClick();
});
}
// 手动上报方法
trackWebclick(el: HTMLElement, props: Record<string, any>) {
const target = el;
const elementInPath = this.getPathElemsByEl(target);
const tagName = target.tagName.toLowerCase();
const className = target.className;
const content = target.innerText;
const selector = elementInPath
.map((item) => this.getElementPathName(item))
.join("");
this.clickData = {
event: "click",
action: "click",
selector,
content,
className,
tagName,
...props,
};
if (tagName === "a") {
const href = target.getAttribute("href");
this.clickData.targetUrl = href;
}
this.trackClick();
}
trackClick() {
this.data = { ...this.data, ...this.clickData };
this.sendData();
}
那么我们实例化了监听器对象之后,即可进行数据监听了:
constructor(props: Record<string, any>) {
this.data = { ...this.data, ...props };
this.initData = { ...this.data, ...props };
this.setPageLoadListener();
this.setElementListener();
this.setPerformanceListener();
window.VITE_MONITOR = this;
}
// 手动点击
function TestMonitor(e: MouseEvent) {
window.VITE_MONITOR.trackWebclick(e.target, {
test: 'hello'
})
}
效果
pageview:
autoclick:
click:
以上简单的埋点数据获取完成