Skip to content

Vue Router 与 RBAC

路由不仅是“URL 对应组件”。在中后台里,它还连接登录态、菜单、权限、数据预取、代码分割和页面恢复。安全边界仍在后端;前端负责体验与可见性。

把权限写成路由元数据

ts
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth: boolean
    permissions?: string[]
    title: string
  }
}

const routes = [
  {
    path: '/activities/publish',
    component: () => import('@/views/activity/PublishView.vue'),
    meta: {
      requiresAuth: true,
      permissions: ['activity:publish'],
      title: '活动发布',
    },
  },
]

类型化 meta 能让新增页面时缺失权限声明直接变成编译问题。页面、菜单和按钮使用同一套权限码,避免三套规则漂移。

导航守卫只做编排

ts
router.beforeEach(async (to) => {
  const auth = useAuthStore()

  if (!to.meta.requiresAuth) return true
  if (!auth.initialized) await auth.restoreSession()
  if (!auth.loggedIn) return { name: 'login', query: { redirect: to.fullPath } }
  if (!auth.hasAny(to.meta.permissions)) return { name: 'forbidden' }
})

推荐返回值风格,而不是依赖容易重复调用或漏调用的 next。守卫不应塞入大段业务逻辑:登录恢复放 Store,权限计算放领域函数,路由层只编排顺序。

动态路由的生命周期

后端菜单驱动路由时,需要稳定的 route name/id 去重,并保存 addRoute 返回的移除函数。登出或切换角色时先移除旧路由,再拉取新权限。否则会出现重复注册、旧权限残留和首屏死循环。

ts
const disposers = serverRoutes.map((route) => router.addRoute(toRouteRecord(route)))

function resetDynamicRoutes() {
  disposers.splice(0).forEach((dispose) => dispose())
}

URL 也是状态容器

列表筛选、分页和排序如果需要分享、刷新恢复或浏览器前进后退,应进入 route.query。弹窗开关、输入中的临时值仍放组件本地。不要把整个 route 深度 watch,监听 route.params.id 或具体 query。

数据请求与竞态

数据可在导航前请求(进入即完整,但等待更久),也可导航后请求(更快展示骨架)。快速切换详情页时使用 AbortController 或请求序号,防止旧请求回写新页面。router.push 后如需改变 UI 状态,要检查 navigation failure。

权限的四层表达

层级前端职责最终校验
菜单是否展示入口后端返回授权菜单/权限
路由是否允许进入接口仍需鉴权
页面是否展示区域与数据数据接口按用户过滤
操作按钮可见/可用写接口二次鉴权与审计

参考:Navigation GuardsRoute Meta FieldsNavigation Failures