人人都会AI编程

权限体系、埋点体系、错误监控体系搭建

更新时间:2026-07-10

在大型 Vue 项目中,业务代码之外还需要搭建三套“隐形基础设施”:权限体系控制谁能访问什么,埋点体系追踪用户行为,错误监控体系及时发现和定位线上问题。下面分别给出可落地的搭建方案。


权限体系

权限体系通常分为路由权限(控制页面访问)和按钮权限(控制功能操作),核心数据来源是后端返回的用户角色或权限列表。

1. 路由权限:动态添加路由

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'

// 静态路由(无需权限即可访问)
const constantRoutes = [
  { path: '/login', component: () => import('@/views/Login.vue') },
  { path: '/404', component: () => import('@/views/404.vue') }
]

// 动态路由(需根据权限添加),meta 中配置所需权限
const asyncRoutes = [
  {
    path: '/admin',
    component: () => import('@/layout/AdminLayout.vue'),
    meta: { permission: 'admin' },
    children: [
      {
        path: 'users',
        component: () => import('@/views/admin/Users.vue'),
        meta: { permission: 'user:list' }
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes: constantRoutes
})

// 全局前置守卫:登录后根据角色动态添加路由
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  if (userStore.token) {
    if (!userStore.hasAddedRoutes) {
      const permissions = userStore.permissions // 从后端获取的权限标识数组
      const accessibleRoutes = filterAsyncRoutes(asyncRoutes, permissions)
      accessibleRoutes.forEach(route => router.addRoute(route))
      userStore.hasAddedRoutes = true
      next({ ...to, replace: true }) // 重新触发路由匹配
    } else {
      next()
    }
  } else {
    if (to.path === '/login') next()
    else next('/login')
  }
})

关键点:filterAsyncRoutes 函数递归筛选出用户有权限的路由,返回的数组通过 router.addRoute 动态挂载。这样不同角色登录后看到的菜单和可访问页面完全不同。

2. 按钮权限:自定义指令

// directives/permission.js
import { useUserStore } from '@/stores/user'

export default {
  mounted(el, binding) {
    const { value } = binding // 需要的权限标识,如 'user:delete'
    const userStore = useUserStore()
    const permissions = userStore.permissions
    if (value && !permissions.includes(value)) {
      el.parentNode?.removeChild(el) // 直接移除按钮元素
    }
  }
}

// 使用
// <button v-permission="'user:delete'">删除用户</button>

更柔和的方式是使用 v-if 结合全局权限判断函数,避免 DOM 被移除后无法恢复。


埋点体系

埋点分为页面埋点(记录页面浏览)和事件埋点(记录按钮点击等),上报通常采用 navigator.sendBeacon 或调用日志接口,避免阻塞页面。

1. 页面浏览埋点:利用路由守卫

// router/index.js
router.afterEach((to) => {
  // 上报页面浏览事件
  trackEvent('page_view', {
    page: to.fullPath,
    title: to.meta.title || document.title,
    timestamp: Date.now()
  })
})

2. 事件埋点:指令 + 组合式函数

<template>
  <button @click="handleClick" v-track:click="{ eventName: 'add_to_cart', params: { sku: '123' } }">
    加入购物车
  </button>
</template>

<script setup>
import { useTracker } from '@/composables/useTracker'

const { track } = useTracker()
function handleClick() {
  // 业务逻辑
}
</script>
// directives/track.js
export default {
  mounted(el, binding) {
    const { arg, value } = binding // arg 是事件类型(click/inview),value 是上报参数
    const handler = () => {
      track(arg, value)
    }
    el._trackHandler = handler
    el.addEventListener(arg, handler)
  },
  unmounted(el, binding) {
    el.removeEventListener(binding.arg, el._trackHandler)
  }
}
// composables/useTracker.js
export function useTracker() {
  function track(eventType, { eventName, params }) {
    // 批量上报或即时上报,可使用 sendBeacon 或 XHR
    const data = { event: eventName, params, timestamp: Date.now() }
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/log', JSON.stringify(data))
    } else {
      fetch('/api/log', { method: 'POST', body: JSON.stringify(data), keepalive: true })
    }
  }
  return { track }
}

3. 曝光埋点(组件进入视口)

<!-- 使用 IntersectionObserver 封装指令 -->
<div v-track:inview="{ eventName: 'banner_show', params: { bannerId: 1 } }">
  <!-- 广告内容 -->
</div>

指令实现时,通过 IntersectionObserver 检测元素可见性,触发一次上报后取消监听。


错误监控体系

错误监控覆盖三大类:JS 运行时错误接口请求错误组件渲染错误。统一收集后上报到 Sentry、自研平台或日志服务。

1. 全局 JS 错误捕获

// main.js
import { createApp } from 'vue'
import { useErrorReporter } from '@/composables/useErrorReporter'

const app = createApp(App)

// Vue 应用级错误处理
app.config.errorHandler = (err, instance, info) => {
  useErrorReporter().report({
    type: 'vue_error',
    message: err.message,
    stack: err.stack,
    component: instance?.$options?.name || 'anonymous',
    info, // 生命周期钩子等信息
    timestamp: Date.now()
  })
}

// 全局 JS 错误(未被 Vue 捕获的)
window.addEventListener('error', (event) => {
  useErrorReporter().report({
    type: 'js_error',
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    timestamp: Date.now()
  })
})

// Promise 未捕获的 rejection
window.addEventListener('unhandledrejection', (event) => {
  useErrorReporter().report({
    type: 'promise_rejection',
    reason: event.reason,
    timestamp: Date.now()
  })
})

2. 接口错误统一拦截:Axios 响应拦截器

// utils/request.js
import axios from 'axios'
import { useErrorReporter } from '@/composables/useErrorReporter'

const instance = axios.create({ baseURL: '/api' })

instance.interceptors.response.use(
  response => response,
  error => {
    // 网络错误或超时
    if (!error.response) {
      useErrorReporter().report({
        type: 'network_error',
        config: error.config,
        message: error.message,
        timestamp: Date.now()
      })
    } else {
      // 服务端返回错误状态码(如 500)
      useErrorReporter().report({
        type: 'api_error',
        status: error.response.status,
        url: error.config.url,
        response: error.response.data,
        timestamp: Date.now()
      })
    }
    return Promise.reject(error)
  }
)

3. 错误上报实现(useErrorReporter)

// composables/useErrorReporter.js
import { useUserStore } from '@/stores/user'

export function useErrorReporter() {
  function report(errorData) {
    // 添加上下文信息:用户、页面 URL、版本号等
    const userStore = useUserStore()
    const payload = {
      ...errorData,
      user: userStore.userInfo?.id,
      page: window.location.href,
      appVersion: import.meta.env.VITE_APP_VERSION,
      timestamp: Date.now()
    }

    // 上报到日志服务(使用 sendBeacon 避免丢数据)
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/log/error', JSON.stringify(payload))
    } else {
      fetch('/api/log/error', {
        method: 'POST',
        body: JSON.stringify(payload),
        keepalive: true
      })
    }
  }

  return { report }
}

4. 生产环境额外保护:Sentry / 自建看板

如需更高阶的功能(sourcemap 解析、错误聚合、告警),可以直接集成 @sentry/vue

import * as Sentry from '@sentry/vue'

Sentry.init({
  app,
  dsn: 'your-dsn',
  environment: 'production',
  release: '1.0.0'
})

这样就能自动捕获 Vue 错误、接口错误,并提供完整上下文。


总结:权限体系用路由守卫+动态路由按钮指令控制访问面,埋点体系用路由钩子+自定义指令解耦业务与追踪,错误监控用全局错误处理+Axios 拦截器+统一上报兜底。三者都是非侵入式基础设施建设,一旦搭建完成,对后续业务迭代几乎零感知,却能为生产环境稳定性和数据分析提供坚实底座。