移除动态路由
适用场景
本指南适用于前端完全掌控路由配置的场景,尤其是权限固定的管理后台,无需依赖后端动态路由。
背景说明
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. 移除动态路由后性能有提升吗?
有一定提升,主要体现在:
- 减少了路由接口请求
- 减少了路由解析时间
- 首次加载更快
总结
移除动态路由适合小型项目和权限固定的场景。如果你的项目符合以下特点,可以考虑使用静态路由:
- 路由结构简单,不经常变动
- 角色权限固定,不需要动态调整
- 团队规模小,沟通成本低
- 追求更简单的代码结构
否则,建议保留动态路由,以获得更好的灵活性和可维护性。
