作为前端工程师,你是否经历过:
产品经理:"用户点击这个按钮的转化率怎么只有 0.5%?" 运营同学:"这个页面曝光量很高,但没人点进去,是不是文案有问题?" 老板:"我们的用户都在页面停留多久?哪些功能最受欢迎?"
面对这些灵魂拷问,没有埋点数据的你,就像一个没有地图的探险家,只能靠猜。
埋点,就是给你的网站装上"监控摄像头",让每一次点击、每一次曝光、每一次停留都有据可查。
类型 | 定义 | 场景 | 难度 |
|---|---|---|---|
点击埋点 | 记录用户点击行为 | 按钮、链接、表单提交 | ⭐⭐ |
曝光埋点 | 记录元素进入视口 | 广告位、卡片、列表项 | ⭐⭐⭐ |
页面埋点 | 记录页面停留时间 | 页面进入/离开时间 | ⭐⭐ |
滚动埋点 | 记录滚动深度 | 内容阅读进度 | ⭐⭐⭐ |
错误埋点 | 记录异常情况 | 接口报错、渲染失败 | ⭐⭐ |
javascript 体验AI代码助手 代码解读复制代码// 一份标准的埋点数据应该长这样
interface TrackEvent {
eventName: string // 事件名称(如:btn_click_login)
eventType: 'click' | 'expose' | 'page' // 事件类型
timestamp: number // 时间戳
pageUrl: string // 当前页面 URL
referrer: string // 来源页面
elementId?: string // 元素 ID
elementClass?: string // 元素类名
position?: { x: number; y: number } // 点击位置
duration?: number // 停留时长
extra?: Record<string, any> // 自定义字段
}javascript 体验AI代码助手 代码解读复制代码// ❌ 反面教材:命名混乱
'tap', 'click', 'btn_click', 'button_click'
// ✅ 正面教材:统一规范
// 格式:[模块]_[类型]_[描述]
'home_btn_login_click' // 首页登录按钮点击
'product_card_expose' // 商品卡片曝光
'list_item_click' // 列表项点击
'page_detail_stay' // 详情页停留手动埋点就像狙击手,精准、可控,但需要一个个标记。适合:
javascript 体验AI代码助手 代码解读复制代码// src/directives/track.js
import { trackEvent } from '../utils/tracker'
export const vTrack = {
mounted(el, binding) {
const { eventName, eventType = 'click', extra = {} } = binding.value
el.addEventListener(eventType, () => {
// 收集元素信息
const elementInfo = {
elementId: el.id,
elementClass: el.className,
text: el.textContent || el.innerText
}
// 上报埋点
trackEvent({
eventName,
eventType,
...elementInfo,
...extra
})
console.log(`🚀 埋点上报: ${eventName}`)
})
}
}vue 体验AI代码助手 代码解读复制代码<template>
<button v-track="{ eventName: 'btn_login_click', extra: { from: 'header' } }">
登录
</button>
<button v-track.click="{ eventName: 'btn_submit_click' }">
提交表单
</button>
<a href="/products" v-track="{ eventName: 'link_products_click' }">
查看商品
</a>
</template>自动埋点就像撒网捕鱼,无需手动标记,自动捕获所有事件。适合:
javascript 体验AI代码助手 代码解读复制代码// src/utils/tracker.js
export class AutoTracker {
constructor() {
this.observer = null
this.init()
}
init() {
// 监听 DOM 变化
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.attachEventListeners(node)
}
})
})
})
// 开始监听整个文档
this.observer.observe(document.body, {
childList: true,
subtree: true
})
}
attachEventListeners(element) {
// 只监听可点击元素
const clickableTags = ['button', 'a', 'input', 'select', 'textarea']
if (clickableTags.includes(element.tagName.toLowerCase())) {
element.addEventListener('click', (e) => {
const eventName = this.generateEventName(e.target)
trackEvent({
eventName,
eventType: 'click',
elementId: e.target.id,
elementClass: e.target.className
})
})
}
// 递归处理子元素
element.querySelectorAll('*').forEach((child) => {
this.attachEventListeners(child)
})
}
generateEventName(element) {
const tag = element.tagName.toLowerCase()
const id = element.id ? `_${element.id}` : ''
const cls = element.className ? `_${element.className.split(' ')[0]}` : ''
return `auto_${tag}${id}${cls}_click`
}
}javascript 体验AI代码助手 代码解读复制代码// src/utils/exposeTracker.js
export class ExposeTracker {
constructor() {
this.observer = null
this.trackedElements = new Set()
this.init()
}
init() {
// IntersectionObserver 配置
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !this.trackedElements.has(entry.target)) {
// 元素进入视口且未被追踪过
this.trackedElements.add(entry.target)
const eventName = entry.target.dataset.trackName || 'element_expose'
trackEvent({
eventName,
eventType: 'expose',
elementId: entry.target.id,
duration: Date.now()
})
console.log(`✨ 曝光追踪: ${eventName}`)
}
})
},
{
threshold: 0.5, // 50%进入视口才算曝光
rootMargin: '0px',
once: true // 只追踪一次
}
)
}
observe(element) {
if (element) {
this.observer.observe(element)
}
}
observeAll(selector) {
document.querySelectorAll(selector).forEach((el) => {
this.observe(el)
})
}
}vue 体验AI代码助手 代码解读复制代码<template>
<div class="product-card" data-track-name="product_card_expose">
<img src="product.jpg" alt="商品图片" />
<h3>商品名称</h3>
<p>¥99.00</p>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { exposeTracker } from '../utils/exposeTracker'
onMounted(() => {
// 监听所有商品卡片
exposeTracker.observeAll('.product-card')
})
</script>页面停留时间 = 离开时间 - 进入时间
javascript 体验AI代码助手 代码解读复制代码// src/utils/pageTracker.js
export class PageTracker {
constructor() {
this.pageStartTime = Date.now()
this.currentPage = window.location.pathname
this.init()
}
init() {
// 页面进入时记录
this.trackPageEnter()
// 监听页面离开
window.addEventListener('beforeunload', () => {
this.trackPageLeave()
})
// 监听路由变化(SPA)
if (window.__VUE_ROUTER__) {
window.__VUE_ROUTER__.afterEach(() => {
this.trackPageLeave()
this.pageStartTime = Date.now()
this.currentPage = window.location.pathname
this.trackPageEnter()
})
}
}
trackPageEnter() {
trackEvent({
eventName: `page_${this.currentPage}_enter`,
eventType: 'page',
timestamp: this.pageStartTime
})
}
trackPageLeave() {
const duration = Date.now() - this.pageStartTime
trackEvent({
eventName: `page_${this.currentPage}_stay`,
eventType: 'page',
duration,
timestamp: Date.now()
})
console.log(`⏱️ 页面停留: ${this.currentPage} - ${duration}ms`)
}
}javascript 体验AI代码助手 代码解读复制代码// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { PageTracker } from './utils/pageTracker'
const app = createApp(App)
// 初始化页面追踪
new PageTracker()
app.use(router).mount('#app')javascript 体验AI代码助手 代码解读复制代码// src/utils/reporter.js
export class Reporter {
constructor() {
this.queue = []
this.maxQueueSize = 10
this.reportInterval = 5000 // 5秒上报一次
this.init()
}
init() {
// 定时上报
setInterval(() => {
this.flush()
}, this.reportInterval)
// 页面卸载前上报剩余数据
window.addEventListener('beforeunload', () => {
this.flush(true)
})
}
add(event) {
// 添加到队列
this.queue.push({
...event,
timestamp: Date.now(),
uuid: this.generateUUID()
})
// 队列满了立即上报
if (this.queue.length >= this.maxQueueSize) {
this.flush()
}
}
async flush(force = false) {
if (this.queue.length === 0) return
const events = [...this.queue]
this.queue = []
try {
// 使用 navigator.sendBeacon 保证页面卸载时也能发送
if (navigator.sendBeacon && !force) {
const data = JSON.stringify(events)
const blob = new Blob([data], { type: 'application/json' })
navigator.sendBeacon('/api/track', blob)
} else {
// 降级方案
await fetch('/api/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(events),
keepalive: true
})
}
console.log(`📤 上报成功: ${events.length} 条数据`)
} catch (error) {
// 上报失败,放回队列
this.queue = [...events, ...this.queue]
console.error('📥 上报失败:', error)
}
}
generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
}javascript 体验AI代码助手 代码解读复制代码// src/utils/tracker.js
import { Reporter } from './reporter'
const reporter = new Reporter()
export const trackEvent = (event) => {
const baseData = {
pageUrl: window.location.href,
referrer: document.referrer,
userAgent: navigator.userAgent,
screenWidth: window.innerWidth,
screenHeight: window.innerHeight
}
reporter.add({
...baseData,
...event
})
}bash 体验AI代码助手 代码解读复制代码src/
├── utils/
│ ├── tracker.js # 核心追踪函数
│ ├── reporter.js # 数据上报模块
│ ├── pageTracker.js # 页面追踪
│ └── exposeTracker.js # 曝光追踪
├── directives/
│ └── track.js # v-track 指令
└── plugins/
└── tracker.js # Vue 插件javascript 体验AI代码助手 代码解读复制代码// src/plugins/tracker.js
import { vTrack } from '../directives/track'
import { AutoTracker } from '../utils/autoTracker'
import { PageTracker } from '../utils/pageTracker'
import { trackEvent } from '../utils/tracker'
export const TrackerPlugin = {
install(app, options = {}) {
// 注册指令
app.directive('track', vTrack)
// 全局方法
app.config.globalProperties.$track = trackEvent
// 初始化追踪器
if (options.autoTrack !== false) {
new AutoTracker()
}
if (options.pageTrack !== false) {
new PageTracker()
}
console.log('🎯 埋点 SDK 初始化完成')
}
}javascript 体验AI代码助手 代码解读复制代码// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import { TrackerPlugin } from './plugins/tracker'
const app = createApp(App)
// 配置埋点插件
app.use(TrackerPlugin, {
autoTrack: true, // 开启自动埋点
pageTrack: true // 开启页面追踪
})
app.mount('#app')arduino 体验AI代码助手 代码解读复制代码vue3-track-demo/
├── src/
│ ├── utils/
│ │ ├── tracker.js
│ │ ├── reporter.js
│ │ ├── pageTracker.js
│ │ └── exposeTracker.js
│ ├── directives/
│ │ └── track.js
│ ├── plugins/
│ │ └── tracker.js
│ ├── components/
│ │ ├── TrackButton.vue
│ │ └── TrackCard.vue
│ ├── App.vue
│ ├── main.js
│ └── router.js
├── public/
├── index.html
├── package.json
└── vite.config.jsvue 体验AI代码助手 代码解读复制代码<!-- src/components/TrackButton.vue -->
<template>
<button
:class="['track-btn', className]"
v-track="{ eventName, extra }"
@click="handleClick"
>
<slot></slot>
</button>
</template>
<script setup>
import { trackEvent } from '../utils/tracker'
const props = defineProps({
eventName: {
type: String,
required: true
},
className: {
type: String,
default: ''
},
extra: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['click'])
const handleClick = () => {
emit('click')
}
</script>
<style scoped>
.track-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
background: #409eff;
color: white;
cursor: pointer;
}
.track-btn:hover {
background: #66b1ff;
}
</style>vue 体验AI代码助手 代码解读复制代码<!-- src/App.vue -->
<template>
<div class="app">
<TrackButton eventName="btn_login_click" :extra="{ from: 'app' }">
登录
</TrackButton>
<TrackButton eventName="btn_signup_click" className="secondary">
注册
</TrackButton>
<div class="cards">
<TrackCard
v-for="product in products"
:key="product.id"
:product="product"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import TrackButton from './components/TrackButton.vue'
import TrackCard from './components/TrackCard.vue'
const products = ref([
{ id: 1, name: '商品1', price: 99 },
{ id: 2, name: '商品2', price: 199 },
{ id: 3, name: '商品3', price: 299 }
])
</script>javascript 体验AI代码助手 代码解读复制代码// src/utils/userId.js
export const getUserId = () => {
let userId = localStorage.getItem('track_user_id')
if (!userId) {
userId = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
localStorage.setItem('track_user_id', userId)
}
return userId
}
// 在上报时添加用户 ID
trackEvent({
eventName: 'btn_click',
userId: getUserId()
})javascript 体验AI代码助手 代码解读复制代码// src/utils/debounce.js
export const debounce = (fn, delay = 300) => {
let timer = null
return (...args) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
// 应用场景:滚动埋点
const handleScroll = debounce(() => {
const scrollTop = window.scrollY
const scrollPercent = (scrollTop / document.body.scrollHeight) * 100
trackEvent({
eventName: 'page_scroll',
scrollPercent: Math.round(scrollPercent)
})
}, 500)
window.addEventListener('scroll', handleScroll)javascript 体验AI代码助手 代码解读复制代码// src/utils/encrypt.js
export const encryptData = (data) => {
const str = JSON.stringify(data)
// 简单的 base64 编码(实际项目请使用更安全的加密方式)
return btoa(encodeURIComponent(str))
}
// 在上报前加密
const encryptedData = encryptData(trackData)javascript 体验AI代码助手 代码解读复制代码// src/utils/tracker.js
const isDev = import.meta.env.DEV
export const trackEvent = (event) => {
if (isDev) {
// 开发环境打印埋点数据
console.group(`🎯 ${event.eventName}`)
console.log('事件数据:', event)
console.groupEnd()
}
reporter.add(event)
}推荐使用 Chrome DevTools 的 Performance 面板和 Network 面板来调试埋点:
/api/track 请求scss 体验AI代码助手 代码解读复制代码┌─────────────────────────────────────────────────────────────────┐
│ 用户行为 │
└───────────────────┬───────────────────┬───────────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 手动埋点 │ │ 自动埋点 │
│ (v-track) │ │ (Mutation) │
└──────┬───────┘ └──────┬───────┘
│ │
└─────────┬─────────┘
▼
┌──────────────────┐
│ trackEvent() │
│ 数据预处理 │
└────────┬─────────┘
▼
┌──────────────────┐
│ Reporter │
│ (队列/定时上报) │
└────────┬─────────┘
▼
┌──────────────────┐
│ API / Beacon │
│ 数据传输 │
└────────┬─────────┘
▼
┌──────────────────┐
│ 后端存储/分析 │
└──────────────────┘✅ 事件命名规范统一 ✅ 数据结构设计合理 ✅ 手动/自动埋点结合 ✅ 曝光追踪使用 IntersectionObserver ✅ 数据上报使用 Beacon API ✅ 支持批量上报和失败重试 ✅ 开发环境有调试日志 ✅ 生产环境关闭调试信息
埋点只是手段,真正的价值在于:
记住:不要为了埋点而埋点,只埋真正有用的数据!
最后:如果你觉得这篇文章对你有帮助,欢迎点击下方的❤️按钮(开个玩笑,这里没有按钮,但是可以给我点个赞哦!)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。