在大型 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 拦截器+统一上报兜底。三者都是非侵入式基础设施建设,一旦搭建完成,对后续业务迭代几乎零感知,却能为生产环境稳定性和数据分析提供坚实底座。