大数跨境
0
0

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

手把手教你设计Vue3项目埋点方案,开箱即用! 前端新次元
2025-11-28
3

 

字数 1506,阅读大约需 8 分钟

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

  1. 1. 用户点击统计 - 记录用户点击了哪些元素
  2. 2. 停留时长统计 - 记录用户在页面的停留时间
  3. 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 更新

👇 关注 | 点赞 | 分享,我们共同进化



🔥 热门文章推荐:


你知道什么是前端HTTP缓存吗?
大厂为什么禁止/不推荐使用 export default
Vue3组件通信难题?用这个API轻松搞定父调子
终于知道为什么很多大厂都会问CSS盒子模型
一行代码解决跨域问题,js新特性解析

【声明】内容源于网络
0
0
前端新次元
聚焦前端核心技术,分享实用干货与深度解析。每日分享 JavaScript、Vue、React等文章。关注我,持续提升开发力!
内容 115
粉丝 0
前端新次元 聚焦前端核心技术,分享实用干货与深度解析。每日分享 JavaScript、Vue、React等文章。关注我,持续提升开发力!
总阅读33
粉丝0
内容115