字数 1506,阅读大约需 8 分钟
假如我们的埋点方案需要实现三个核心功能:
-
1. 用户点击统计 - 记录用户点击了哪些元素 -
2. 停留时长统计 - 记录用户在页面的停留时间 -
3. 错误日志收集 - 捕获并上报应用错误 -
-
技术选型与准备
选择Vue3的原因
Vue3提供更好的性能。Vue3有更灵活的Composition API。Vue3的TypeScript支持更好。
所需依赖
安装vue-router
npm install vue-router@4
我们使用原生JavaScript实现。不需要额外安装埋点库。这样可以减少包体积。
核心实现步骤
第一步:创建埋点类型定义
// types/tracking.ts
export interface ClickEvent {
type: 'click'
element: string
page: string
timestamp: number
}
export interface PageViewEvent {
type: 'pageview'
page: string
duration: number
timestamp: number
}
export interface ErrorEvent {
type: 'error'
message: string
stack?: string
page: string
timestamp: number
}
export type TrackingEvent = ClickEvent | PageViewEvent | ErrorEvent
第二步:实现埋点核心类
// utils/tracker.ts
class Tracker {
private queue: TrackingEvent[] = []
private readonly maxRetry = 3
private readonly batchSize = 10
// 发送事件到服务器
private async sendToServer(events: TrackingEvent[]): Promise<void> {
try {
const response = await fetch('/api/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ events }),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
} catch (error) {
console.warn('埋点发送失败:', error)
throw error
}
}
// 添加事件到队列
track(event: TrackingEvent): void {
this.queue.push(event)
// 达到批量大小就发送
if (this.queue.length >= this.batchSize) {
this.flush()
}
}
// 强制发送所有事件
async flush(): Promise<void> {
if (this.queue.length === 0) return
const events = [...this.queue]
this.queue = []
for (let attempt = 1; attempt <= this.maxRetry; attempt++) {
try {
await this.sendToServer(events)
break
} catch (error) {
if (attempt === this.maxRetry) {
console.error('埋点发送最终失败:', error)
// 这里可以存储到localStorage,下次重试
}
}
}
}
}
export const tracker = new Tracker()
第三步:实现Vue3指令
// directives/trackClick.ts
import { tracker } from '@/utils/tracker'
export const trackClick = {
mounted(el: HTMLElement, binding: any) {
const trackData = binding.value
el.addEventListener('click', () => {
const event = {
type: 'click' as const,
element: trackData.element || el.tagName,
page: window.location.pathname,
timestamp: Date.now()
}
tracker.track(event)
})
}
}
第四步:实现页面停留时长统计
// composables/usePageTrack.ts
import { ref, onMounted, onUnmounted } from 'vue'
import { tracker } from '@/utils/tracker'
export function usePageTrack(pageName: string) {
const startTime = ref(0)
onMounted(() => {
startTime.value = Date.now()
})
onUnmounted(() => {
const endTime = Date.now()
const duration = endTime - startTime.value
const event = {
type: 'pageview' as const,
page: pageName,
duration,
timestamp: endTime
}
tracker.track(event)
})
}
第五步:全局错误捕获
// utils/errorHandler.ts
import { tracker } from './tracker'
export function setupErrorTracking(): void {
// Vue错误处理
const originalErrorHandler = Vue.config.errorHandler
Vue.config.errorHandler = (err, vm, info) => {
const errorEvent = {
type: 'error' as const,
message: err.message,
stack: err.stack,
page: window.location.pathname,
timestamp: Date.now(),
component: info
}
tracker.track(errorEvent)
// 调用原来的错误处理
if (originalErrorHandler) {
originalErrorHandler.call(vm, err, vm, info)
}
}
// 全局JavaScript错误
window.addEventListener('error', (event) => {
const errorEvent = {
type: 'error' as const,
message: event.message,
stack: event.error?.stack,
page: window.location.pathname,
timestamp: Date.now()
}
tracker.track(errorEvent)
})
// Promise rejection
window.addEventListener('unhandledrejection', (event) => {
const errorEvent = {
type: 'error' as const,
message: event.reason?.message || 'Unhandled Promise Rejection',
stack: event.reason?.stack,
page: window.location.pathname,
timestamp: Date.now()
}
tracker.track(errorEvent)
})
}
在Vue3项目中集成
主文件配置
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { trackClick } from './directives/trackClick'
import { setupErrorTracking } from './utils/errorHandler'
const app = createApp(App)
// 注册全局指令
app.directive('track-click', trackClick)
// 设置错误追踪
setupErrorTracking()
app.use(router)
app.mount('#app')
路由配置
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
trackPage: true,
pageName: '首页'
}
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue'),
meta: {
trackPage: true,
pageName: '关于页面'
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫处理页面追踪
router.afterEach((to) => {
if (to.meta.trackPage) {
// 这里可以触发页面浏览事件
const event = {
type: 'pageview' as const,
page: to.meta.pageName as string,
timestamp: Date.now(),
duration: 0 // 离开时更新
}
// tracker.track(event)
}
})
export default router
在组件中使用
<!-- components/ProductList.vue -->
<template>
<div>
<h1>产品列表</h1>
<button
v-track-click="{ element: 'filter-button' }"
@click="handleFilter"
>
筛选
</button>
<div
v-for="product in products"
:key="product.id"
class="product-item"
v-track-click="{ element: `product-${product.id}` }"
@click="viewProduct(product)"
>
{{ product.name }}
</div>
</div>
</template>
<script setup lang="ts">
import { usePageTrack } from '@/composables/usePageTrack'
// 页面停留统计
usePageTrack('产品列表页')
const products = [
{ id: 1, name: '产品A' },
{ id: 2, name: '产品B' }
]
const viewProduct = (product: any) => {
// 处理产品查看逻辑
}
</script>
一些扩展功能
性能监控
// utils/performanceTracker.ts
import { tracker } from './tracker'
export function trackPerformance(): void {
// 监控页面加载性能
window.addEventListener('load', () => {
const navigationTiming = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
const perfEvent = {
type: 'performance' as const,
metric: 'page_load',
value: navigationTiming.loadEventEnd - navigationTiming.navigationStart,
timestamp: Date.now()
}
tracker.track(perfEvent)
})
// 监控资源加载
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
const resourceEvent = {
type: 'performance' as const,
metric: 'resource_load',
name: entry.name,
value: entry.duration,
timestamp: Date.now()
}
tracker.track(resourceEvent)
})
})
observer.observe({ entryTypes: ['resource'] })
}
用户行为路径追踪
// utils/userJourney.ts
class UserJourney {
private steps: string[] = []
private startTime: number = Date.now()
addStep(step: string): void {
this.steps.push(step)
// 每5步发送一次数据
if (this.steps.length % 5 === 0) {
this.flushJourney()
}
}
private flushJourney(): void {
const journeyEvent = {
type: 'user_journey' as const,
steps: [...this.steps],
duration: Date.now() - this.startTime,
timestamp: Date.now()
}
tracker.track(journeyEvent)
this.steps = []
this.startTime = Date.now()
}
}
export const userJourney = new UserJourney()
数据格式示例
点击事件数据
{
"type": "click",
"element": "login-button",
"page": "/login",
"timestamp": 1700000000000
}
页面浏览数据
{
"type": "pageview",
"page": "用户首页",
"duration": 45000,
"timestamp": 1700000000000
}
错误日志数据
{
"type": "error",
"message": "Cannot read property 'name' of undefined",
"stack": "TypeError: Cannot read property...",
"page": "/user/profile",
"timestamp": 1700000000000
}
优化建议
性能优化
-
• 使用批量发送减少请求次数 -
• 设置合适的批量大小 -
• 实现请求失败重试机制 -
• 考虑使用Web Worker处理数据
数据准确性
-
• 处理页面可见性变化 -
• 考虑单页应用路由变化 -
• 处理浏览器标签页切换 -
• 实现数据采样避免数据过多
隐私保护
-
• 提供用户选择退出机制 -
• 避免收集个人身份信息 -
• 数据匿名化处理 -
• 遵守相关数据保护法规
部署注意事项
环境配置
// config/tracking.ts
export const trackingConfig = {
// 开发环境不发送真实数据
enabled: process.env.NODE_ENV === 'production',
// 采样率
samplingRate: 0.1,
// 批量大小
batchSize: 10,
// 发送间隔
flushInterval: 30000
}
这个埋点方案提供了完整的数据收集能力。方案易于扩展。方案性能良好。你可以根据具体需求调整实现细节。
🚀专注前沿技术拆解 | 每日 9:00 更新
👇 关注 | 点赞 | 分享,我们共同进化
🔥 热门文章推荐:

