Vue Router 4 动态路由与权限控制的最佳实践方案

在中后台管理系统中,基于角色的权限控制几乎是绕不开的需求。前端需要根据用户身份动态决定能访问哪些页面、看到哪些菜单。Vue Router 4 配合 Vue 3 的组合式 API,为这类场景提供了清晰的实现路径。本文将从路由结构设计、动态路由生成、导航守卫控制到按钮级权限,给出一套可落地的完整方案。

一、整体设计思路

权限控制的核心逻辑可以概括为三步:

  1. 路由分层:将路由分为「静态路由」和「动态路由」两类。静态路由是所有用户都能访问的页面(如登录页、404 页),动态路由则根据用户权限按需添加。
  2. 登录后拉取权限:用户登录成功后,前端向后端请求用户信息与权限标识,据此计算出该用户可访问的路由表。
  3. 动态注册路由:通过 router.addRoute() 将计算好的路由动态添加到路由器实例中,同时生成对应的菜单。

这种方式的优势在于:未授权的路由根本不会被注册到路由器中,用户即使手动输入 URL 也无法访问,安全性比单纯在守卫里判断更高。

二、路由结构设计

建议将所有路由集中管理,每个路由的 meta 字段承载权限信息:

// router/routes.js
export const constantRoutes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/login/index.vue'),
    meta: { title: '登录', hidden: true }
  },
  {
    path: '/404',
    name: 'NotFound',
    component: () => import('@/views/404.vue'),
    meta: { title: '404', hidden: true }
  }
]

export const asyncRoutes = [
  {
    path: '/',
    component: () => import('@/layout/index.vue'),
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        meta: { title: '仪表盘', icon: 'dashboard', roles: ['admin', 'editor'] }
      }
    ]
  },
  {
    path: '/system',
    component: () => import('@/layout/index.vue'),
    meta: { title: '系统管理', icon: 'setting', roles: ['admin'] },
    children: [
      {
        path: 'user',
        name: 'UserManage',
        component: () => import('@/views/system/user.vue'),
        meta: { title: '用户管理', roles: ['admin'] }
      },
      {
        path: 'role',
        name: 'RoleManage',
        component: () => import('@/views/system/role.vue'),
        meta: { title: '角色管理', roles: ['admin'] }
      }
    ]
  }
]

关键约定:

  • meta.roles 声明该路由允许访问的角色列表,不写则表示所有已登录用户可访问。
  • meta.hidden 控制该路由是否在侧边菜单中隐藏。
  • 父级路由的 roles 应包含所有子路由的角色并集,避免菜单渲染时出现空目录。

三、动态路由生成与注册

核心工具函数是根据用户角色过滤路由表:

// utils/permission.js
export function filterAsyncRoutes(routes, roles) {
  const res = []
  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })
  return res
}

function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  }
  return true
}

在 Pinia store 中管理路由状态:

// stores/permission.js
import { defineStore } from 'pinia'
import { constantRoutes, asyncRoutes } from '@/router/routes'
import { filterAsyncRoutes } from '@/utils/permission'

export const usePermissionStore = defineStore('permission', {
  state: () => ({
    routes: [],
    addRoutes: []
  }),
  actions: {
    generateRoutes(roles) {
      return new Promise(resolve => {
        const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
        this.addRoutes = accessedRoutes
        this.routes = constantRoutes.concat(accessedRoutes)
        resolve(accessedRoutes)
      })
    }
  }
})

注意:Vue Router 4 中 addRoute 返回一个移除函数,在需要重置路由(如切换账号)时非常有用。

四、导航守卫:串联整个流程

全局前置守卫负责判断登录状态与路由注册状态:

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

const whiteList = ['/login', '/404']

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  const permissionStore = usePermissionStore()

  if (userStore.token) {
    if (to.path === '/login') {
      next({ path: '/' })
    } else {
      // 已获取用户信息,直接放行
      if (userStore.roles.length > 0) {
        next()
      } else {
        try {
          const { roles } = await userStore.getInfo()
          const accessRoutes = await permissionStore.generateRoutes(roles)
          accessRoutes.forEach(route => router.addRoute(route))
          // 关键:replace 确保 addRoute 后重新匹配
          next({ ...to, replace: true })
        } catch (err) {
          userStore.resetToken()
          next(`/login?redirect=${to.path}`)
        }
      }
    }
  } else {
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
    }
  }
})

这里有两个容易踩坑的点:

  • next({ ...to, replace: true }):动态添加路由后,必须重新触发一次导航,否则当前导航仍匹配的是旧路由表,可能落到 404。
  • 兜底 404 路由:必须在动态路由添加完成后,再追加 { path: '/:pathMatch(.*)*', redirect: '/404' },否则会导致未注册路由被误匹配。

五、菜单渲染与按钮级权限

侧边栏菜单直接基于 permissionStore.routes 递归渲染即可,过滤掉 meta.hidden 为 true 的项。

按钮级权限则通过自定义指令实现:

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

export const permission = {
  mounted(el, binding) {
    const userStore = useUserStore()
    const { value } = binding
    if (value && value.length) {
      const hasPerm = userStore.permissions.some(p => value.includes(p))
      if (!hasPerm) {
        el.parentNode?.removeChild(el)
      }
    }
  }
}

使用方式:

<template>
  <el-button v-permission="['user:delete']">删除</el-button>
</template>

六、几个实践建议

第一,动态路由不要持久化到本地。 每次刷新页面都应重新拉取用户信息并生成路由,否则角色变更后会出现权限不一致。

第二,退出登录时重置路由。 保存 addRoute 返回的移除函数,在登出时逐一调用,或直接 location.reload() 简单粗暴地重置。

第三,后端才是权限的最终防线。 前端路由控制只解决「用户看不到」,接口层面的鉴权必须由后端完成,前端方案是体验优化而非安全边界。

第四,角色与权限标识分离。 角色(role)用于路由级控制,权限点(permission)用于按钮级控制,两者粒度不同,混用会让后期维护变得困难。

小结

Vue Router 4 的动态路由方案本质上是「按需注册 + 守卫串联 + 状态管理」三者的配合。把路由分层设计清楚,用 meta 承载权限元信息,在守卫中完成异步注册与重定向,再配合菜单渲染和按钮指令,就能覆盖绝大多数中后台系统的权限需求。方案本身不复杂,真正需要留心的是那些边界情况——刷新、登出、404 兜底、路由重匹配——处理好这些细节,整套权限体系才算真正稳固。

未经允许不得转载:任鹏个人博客 » Vue Router 4 动态路由与权限控制的最佳实践方案

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏