手把手教你设计Vue3项目埋点方案,开箱即用

5 阅读3分钟

假如我们的埋点方案需要实现三个核心功能:

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'
  elementstring
  pagestring
  timestampnumber
}

export interface PageViewEvent {
  type'pageview'
  pagestring
  durationnumber
  timestampnumber
}

export interface ErrorEvent {
  type'error'
  messagestring
  stack?: string
  pagestring
  timestampnumber
}

export type TrackingEvent = ClickEvent | PageViewEvent | ErrorEvent

第二步:实现埋点核心类

// utils/tracker.ts
class Tracker {
  private queueTrackingEvent[] = []
  private readonly maxRetry = 3
  private readonly batchSize = 10

  // 发送事件到服务器
  private async sendToServer(eventsTrackingEvent[]): Promise<void> {
    try {
      const response = await fetch('/api/track', {
        method'POST',
        headers: {
          'Content-Type''application/json',
        },
        bodyJSON.stringify({ events }),
      })
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
    } catch (error) {
      console.warn('埋点发送失败:', error)
      throw error
    }
  }

  // 添加事件到队列
  track(eventTrackingEvent): void {
    this.queue.push(event)
    
    // 达到批量大小就发送
    if (this.queue.length >= this.batchSize) {
      this.flush()
    }
  }

  // 强制发送所有事件
  async flush(): Promise<void> {
    if (this.queue.length === 0return

    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,
        pagewindow.location.pathname,
        timestampDate.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,
      pagewindow.location.pathname,
      timestampDate.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,
      pagewindow.location.pathname,
      timestampDate.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,
      pagewindow.location.pathname,
      timestampDate.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: { 
      trackPagetrue,
      pageName'首页'
    }
  },
  {
    path'/about',
    name'About', 
    component() => import('@/views/About.vue'),
    meta: {
      trackPagetrue,
      pageName'关于页面'
    }
  }
]

const router = createRouter({
  historycreateWebHistory(),
  routes
})

// 路由守卫处理页面追踪
router.afterEach((to) => {
  if (to.meta.trackPage) {
    // 这里可以触发页面浏览事件
    const event = {
      type'pageview' as const,
      page: to.meta.pageName as string,
      timestampDate.now(),
      duration0 // 离开时更新
    }
    // 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 = [
  { id1name'产品A' },
  { id2name'产品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')[0as PerformanceNavigationTiming
    
    const perfEvent = {
      type'performance' as const,
      metric'page_load',
      value: navigationTiming.loadEventEnd - navigationTiming.navigationStart,
      timestampDate.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,
        timestampDate.now()
      }
      
      tracker.track(resourceEvent)
    })
  })
  
  observer.observe({ entryTypes: ['resource'] })
}

用户行为路径追踪

// utils/userJourney.ts
class UserJourney {
  private stepsstring[] = []
  private startTimenumber = Date.now()
  
  addStep(stepstring): 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],
      durationDate.now() - this.startTime,
      timestampDate.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
}

这个埋点方案提供了完整的数据收集能力。方案易于扩展。方案性能良好。你可以根据具体需求调整实现细节。