Skip to content

移除动态路由

适用场景

本指南适用于前端完全掌控路由配置的场景,尤其是权限固定的管理后台,无需依赖后端动态路由。

背景说明

vue3-element-admin 默认使用动态路由模式,路由配置从后端获取。这种方式适合大型系统,但对于小型项目或权限固定的系统,可以使用静态路由来简化开发。

动态路由 vs 静态路由

对比项动态路由静态路由
配置方式后端数据库配置前端代码配置
灵活性高,可动态调整低,需修改代码
复杂度
适用场景大型系统,多角色小型系统,固定权限

改造步骤

第一步:调整路由守卫

修改 src/plugins/permission.ts 文件,移除动态路由生成逻辑:

修改前

typescript
// src/plugins/permission.ts
router.beforeEach(async (to, from, next) => {
  const isLogin = !!getToken()

  if (isLogin) {
    if (to.path === '/login') {
      next({ path: '/' })
    } else {
      const permissionStore = usePermissionStore()

      // 判断路由是否加载完成
      if (permissionStore.isRoutesLoaded) {
        if (to.matched.length === 0) {
          next('/404')
        } else {
          next()
        }
      } else {
        try {
          // 生成动态路由
          const dynamicRoutes = await permissionStore.generateRoutes()
          dynamicRoutes.forEach((route) => router.addRoute(route))
          next({ ...to, replace: true })
        } catch (error) {
          console.error(error)
          await useUserStore().clearUserData()
          redirectToLogin(to, next)
        }
      }
    }
  } else {
    // 未登录逻辑
    if (whiteList.includes(to.path)) {
      next()
    } else {
      redirectToLogin(to, next)
    }
  }
})

修改后

typescript
// src/plugins/permission.ts
router.beforeEach(async (to, from, next) => {
  NProgress.start()

  const isLogin = !!getToken()

  if (isLogin) {
    if (to.path === '/login') {
      next({ path: '/' })
    } else {
      // 移除动态路由,改为静态路由
      usePermissionStore().generateRoutes()

      if (to.matched.length === 0) {
        next('/404')
      } else {
        // 动态设置页面标题
        const title = (to.params.title as string) || (to.query.title as string)
        if (title) {
          to.meta.title = title
        }
        next()
      }
    }
  } else {
    // 未登录逻辑
    if (whiteList.includes(to.path)) {
      next()
    } else {
      redirectToLogin(to, next)
    }
  }
})

第二步:修改路由生成逻辑

修改 src/store/modules/permission.ts 文件:

修改前

typescript
// src/store/modules/permission.ts
export const usePermissionStore = defineStore('permission', () => {
  const routes = ref<RouteRecordRaw[]>([])
  const isRoutesLoaded = ref(false)

  /**
   * 生成动态路由
   */
  function generateRoutes() {
    return new Promise<RouteRecordRaw[]>((resolve, reject) => {
      MenuAPI.getRoutes()
        .then((data) => {
          const dynamicRoutes = parseDynamicRoutes(data)
          routes.value = [...constantRoutes, ...dynamicRoutes]
          isRoutesLoaded.value = true
          resolve(dynamicRoutes)
        })
        .catch((error) => {
          reject(error)
        })
    })
  }

  return {
    routes,
    isRoutesLoaded,
    generateRoutes
  }
})

修改后

typescript
// src/store/modules/permission.ts
export const usePermissionStore = defineStore('permission', () => {
  const routes = ref<RouteRecordRaw[]>([])

  /**
   * 生成静态路由(仅用于菜单显示)
   */
  function generateRoutes() {
    // 直接使用静态路由
    routes.value = constantRoutes
  }

  return {
    routes,
    generateRoutes
  }
})

第三步:配置静态路由

src/router/index.ts 文件中配置所有路由:

typescript
// src/router/index.ts
import Layout from '@/layout/index.vue'

export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    meta: { hidden: true }
  },

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        name: 'Dashboard',
        meta: {
          title: 'dashboard',
          icon: 'homepage',
          affix: true,
          keepAlive: true
        }
      }
    ]
  },

  // 系统管理
  {
    path: '/system',
    component: Layout,
    redirect: '/system/user',
    meta: {
      title: '系统管理',
      icon: 'system'
    },
    children: [
      {
        path: 'user',
        name: 'User',
        component: () => import('@/views/system/user/index.vue'),
        meta: {
          title: '用户管理',
          icon: 'user'
        }
      },
      {
        path: 'role',
        name: 'Role',
        component: () => import('@/views/system/role/index.vue'),
        meta: {
          title: '角色管理',
          icon: 'role'
        }
      },
      {
        path: 'menu',
        name: 'Menu',
        component: () => import('@/views/system/menu/index.vue'),
        meta: {
          title: '菜单管理',
          icon: 'menu'
        }
      },
      {
        path: 'dept',
        name: 'Dept',
        component: () => import('@/views/system/dept/index.vue'),
        meta: {
          title: '部门管理',
          icon: 'dept'
        }
      },
      {
        path: 'dict',
        name: 'Dict',
        component: () => import('@/views/system/dict/index.vue'),
        meta: {
          title: '字典管理',
          icon: 'dict'
        }
      }
    ]
  },

  // 404 页面
  {
    path: '/:pathMatch(.*)*',
    component: () => import('@/views/error-page/404.vue'),
    meta: { hidden: true }
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes,
  scrollBehavior: () => ({ left: 0, top: 0 })
})

export default router

权限控制

移除动态路由后,权限控制需要调整为基于按钮权限的方式。

方式一:v-hasPerm 指令

vue
<template>
  <el-button v-hasPerm="'sys:user:add'" @click="handleAdd">
    新增
  </el-button>

  <el-button v-hasPerm="'sys:user:edit'" @click="handleEdit">
    编辑
  </el-button>

  <el-button v-hasPerm="'sys:user:delete'" @click="handleDelete">
    删除
  </el-button>
</template>

方式二:hasPerm 函数

vue
<template>
  <el-button v-if="hasPerm('sys:user:add')" @click="handleAdd">
    新增
  </el-button>
</template>

<script setup lang="ts">
import { usePermissionStore } from '@/store/modules/permission'

const permissionStore = usePermissionStore()

const hasPerm = (permission: string) => {
  return permissionStore.permissions.includes(permission)
}
</script>

权限数据获取

权限数据仍然从后端获取,存储在 Store 中:

typescript
// src/store/modules/user.ts
export const useUserStore = defineStore('user', () => {
  const permissions = ref<string[]>([])

  /**
   * 获取用户信息和权限
   */
  async function getUserInfo() {
    try {
      const res = await UserAPI.getInfo()
      permissions.value = res.data.perms
    } catch (error) {
      return Promise.reject(error)
    }
  }

  return {
    permissions,
    getUserInfo
  }
})

优缺点分析

优点

  • 简化开发:无需维护后端路由表
  • 降低复杂度:前端完全控制路由
  • 易于调试:路由配置一目了然
  • 减少请求:不需要请求路由接口

缺点

  • 灵活性降低:修改路由需要发布前端
  • 权限粗糙:只能控制按钮级别,无法动态控制菜单
  • 不适合多角色:角色较多时,路由配置会很冗余

适用场景

适合使用静态路由

  • 小型管理系统
  • 权限结构固定
  • 角色数量少(<5个)
  • 团队规模小

适合使用动态路由

  • 大型管理系统
  • 权限结构复杂
  • 角色数量多(>5个)
  • 需要灵活调整权限
  • 多租户系统

完整示例

查看完整的静态路由配置示例:

常见问题

1. 移除动态路由后,如何控制菜单显示?

可以在路由的 meta 中添加 roles 字段,根据用户角色过滤菜单:

typescript
{
  path: 'user',
  meta: {
    title: '用户管理',
    roles: ['admin', 'editor'] // 只有 admin 和 editor 可以看到
  }
}

2. 如何处理多角色的路由?

可以为每个角色配置独立的路由,然后根据用户角色加载对应的路由配置。

3. 移除动态路由后性能有提升吗?

有一定提升,主要体现在:

  • 减少了路由接口请求
  • 减少了路由解析时间
  • 首次加载更快

总结

移除动态路由适合小型项目和权限固定的场景。如果你的项目符合以下特点,可以考虑使用静态路由:

  1. 路由结构简单,不经常变动
  2. 角色权限固定,不需要动态调整
  3. 团队规模小,沟通成本低
  4. 追求更简单的代码结构

否则,建议保留动态路由,以获得更好的灵活性和可维护性。

基于 MIT 许可发布