- Published on
Burying Point design
- Authors
- Name
- MASON Joey
- https://x.com/JoeyJoeMA
how
Use useContext to obtain the public information of buried points. When the public information changes, it will be updated uniformly.
Use useRef to get DOM elements.
Use useCallback to cache the reported information reportMessage method, which obtains the useContext content. Treat context as a dependency and redeclare the reportMessage function when the dependency changes.
Use useEffect to listen to DOM events, use reportMessage as a dependency, bind the event in useEffect, and the returned destruction function is used to unbind.
export const LogContext = createContext({});
export const useLog = () => {
/* 定义一些公共参数 */
const message = useContext(LogContext);
const listenDOM = useRef(null);
/* 分清依赖关系 */
const reportMessage = useCallback(
(data, type) => {
if (type === "pv") {
// 页面浏览量上报
console.log("组件 pv 上报", message);
} else if (type === "click") {
// 点击上报
console.log("组件 click 上报", message, data);
}
},
[message]
);
useEffect(() => {
const handleClick = (e) => {
reportMessage(e.target, "click");
};
if (listenDOM.current) {
listenDOM.current.addEventListener("click", handleClick);
}
return function () {
listenDOM.current &&
listenDOM.current.removeEventListener("click", handleClick);
};
}, [reportMessage]);
return [listenDOM, reportMessage];
};
export const LogContext = createContext({});
export const useLog = () => {
/* 定义一些公共参数 */
const message = useContext(LogContext);
const listenDOM = useRef(null);
/* 分清依赖关系 */
const reportMessage = useCallback(
(data, type) => {
if (type === "pv") {
// 页面浏览量上报
console.log("组件 pv 上报", message);
} else if (type === "click") {
// 点击上报
console.log("组件 click 上报", message, data);
}
},
[message]
);
useEffect(() => {
const handleClick = (e) => {
reportMessage(e.target, "click");
};
if (listenDOM.current) {
listenDOM.current.addEventListener("click", handleClick);
}
return function () {
listenDOM.current &&
listenDOM.current.removeEventListener("click", handleClick);
};
}, [reportMessage]);
return [listenDOM, reportMessage];
};
import React, { useState } from "react";
import { LogContext, useLog } from "./hooks/useLog";
const Home = () => {
const [dom, reportMessage] = useLog();
return (
<div>
{/* 监听内部点击 */}
<div ref={dom}>
<button> 按钮 1 (内部点击) </button>
<button> 按钮 2 (内部点击) </button>
<button> 按钮 3 (内部点击) </button>
</div>
{/* 外部点击 */}
<button onClick={reportMessage}>外部点击</button>
</div>
);
};
// 阻断 useState 的更新效应
const Index = React.memo(Home);
const App = () => {
const [value, setValue] = useState({});
return (
<LogContext.Provider value={value}>
<Index />
<button onClick={() => setValue({ cat: "小猫", color: "棕色" })}>
点击
</button>
</LogContext.Provider>
);
};
export default App;
typescript
// 这个是埋点 API
import { AsyncTrackQueue } from "./async-track-queue";
export interface TrackQueue {
seqId: number;
id: number;
timestamp: number;
}
export interface UserTrackData {
type: string;
data: any;
}
// 思考 1:每次调用时是否立马发起请求?
// 答案 1:不一定,比如滚动了页面,那可能存在几十个埋点请求,所以应该先收集一波,然后统一发送。这样服务器的 QPS 会减少
export class BaseTrack extends AsyncTrackQueue<TrackQueue> {
private seq = 0;
/**
* 埋点请求收集
*
* @param data 用户轨迹数据
*/
track(data: UserTrackData) {
// 埋点请求收集
this.addTask({
id: Math.random(),
seqId: this.seq++,
timestamp: Date.now(),
...data,
});
}
/**
* 消费埋点请求任务队列
*
* @param data 任务队列数据,类型为任意类型数组
* @returns 返回一个 Promise,当 img 标签加载完成后 resolve 为 true
*/
comsumeTaskQuene(data: Array<TrackQueue>) {
return new Promise((resolve) => {
// 通过构建一个 img 标签,然后设置 src 属性,来发送请求
const image = new Image();
image.src = "http://localhost:3001/track?data=" + JSON.stringify(data);
console.log("[ comsumeTaskQuene data ] >", data);
image.onload = () => {
resolve(true);
};
});
}
}
// 第二层:AsyncTrackQueue 是专门处理收集工作的
import { debounce } from "lodash";
interface RequiredData {
timestamp: number | string;
}
// 思考 2:如何收集?收集多少?怎么发请求?
export abstract class AsyncTrackQueue<T extends RequiredData> {
_queueData: Array<T> = [];
// 获取本次存储服务
private get storageService() {
return TaskQueueStorableHelper.getInstance();
}
private get queueData(): Array<T> {
return this.storageService.queueData;
}
private set queueData(data: Array<T>) {
this.storageService.queueData = data;
if (data.length) {
this.debounceRun();
}
}
// 添加任务
addTask(data: T | Array<T>) {
this.queueData = this.queueData.concat(data);
}
// 消费埋点请求任务队列,需要子类去实现
protected abstract comsumeTaskQuene(data: Array<T>): Promise<unknown>;
// 上报策略:当一段时间内,没有新增的任务时,可以去上报一波
// 通过 debounce 防抖,来控制上报频率
protected debounceRun = debounce(this.run.bind(this), 500);
private run() {
const currentDataList = this.queueData;
if (currentDataList.length) {
// 清空任务
this.queueData = [];
// 执行任务
this.comsumeTaskQuene(currentDataList);
}
}
}
// 思考 3:当还有数据未上报时,用户关闭了浏览器,那就会丢失一部分待上报的埋点数据,如何解决这个问题?
// 答案 3:使用 localStorage 存储,当用户关闭浏览器时,将数据存到 localStorage 中,下次打开浏览器时,再从 localStorage 中读取数据上报
class TaskQueueStorableHelper<T extends RequiredData = any> {
// 一个单例模式
private static instance: TaskQueueStorableHelper | null = null;
static getInstance<T extends RequiredData = any>() {
if (!this.instance) {
this.instance = new TaskQueueStorableHelper();
}
return this.instance;
}
private STORAGE_KEY = "track-queue";
protected store: any = null;
// 打开浏览器时,是要执行 constructor
constructor() {
const localStorageVal = localStorage.getItem(this.STORAGE_KEY);
if (localStorageVal) {
// 说明有待上报的数据,则存储一些
try {
this.store = JSON.parse(localStorageVal);
} catch (error: any) {
throw new Error(error);
}
}
}
get queueData() {
return this.store?.queueData || [];
}
set queueData(data: Array<T>) {
this.store = {
...this.store,
queueData: data.sort((a, b) => Number(a.timestamp) - Number(b.timestamp)),
};
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.store));
}
}
import { BaseTrack, UserTrackData } from "./track";
// 性能上报
export class Performance {
static readonly timing = window.performance && window.performance.timing;
static init() {
if (!this.timing) {
console.warn("performance is not support");
}
window.addEventListener("load", () => {
const data = this.getTiming();
new BaseTrack().track(data);
});
}
static getTiming(): UserTrackData {
const timing = this.timing;
return {
type: "performance",
data: {
loadTime: timing.loadEventEnd - timing.navigationStart,
domReadyTime: timing.domComplete - timing.domLoading,
readyTime: timing.domContentLoadedEventEnd - timing.navigationStart,
requestTime: timing.responseEnd - timing.requestStart,
},
};
}
}
// 主动上报
const t = new BaseTrack();
export const sendLog = <T>(data: T) => {
t.track(data as T & UserTrackData);
};
an implementation method of front-end buried points, using "layered design", and talked about the logic of buried point collection, local storage, buried point reporting
Load Comments
Table of Contents
0% read