一开始打算做两种模式的路由权限,最后还是分成了3种,分别是:
- 前端固定路由,所有路由是固定的,通过权限过滤菜单和显示
- 前端动态路由,通过权限过滤路由表和菜单
- 后端动态路由,获取接口返回数据,挂载路由表和菜单
VITE_PERMISSION_MODE = 'CONSTANT'
# VITE_PERMISSION_MODE = 'FRONT'
# VITE_PERMISSION_MODE = 'BACK'
因为权限配置灵活,实际工作中一般采用第3种。在个人demo或者权限简单的项目中,第一种也够用。
简要流程
下图是一个简单流程说明,主要在路由的全局前置守卫router.beforeEach
体现。
各个模块的路由对应views,分文件放在router/modules
,方便后面统一使用
路由入口文件 router/index.ts
- 存放系统固定的几个路由(登录、注册、403、404…)
const constantRoutes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
redirect: '/dashboard',
hidden: true,
meta: {
title: 'home'
}
},
{
path: '/login',
name: 'Login',
hidden: true,
meta: {
title: 'signIn'
},
component: () => import(/* webpackChunkName: "login" */ '../views/login/login.vue')
},
{
path: '/403',
name: '403',
hidden: true,
meta: {
title: '没有权限'
},
component: () => import(/* webpackChunkName: "400" */ '../views/403.vue')
}
]
const lastRoutes = [
{
path: '/:pathMatch(.*)*',
name: '404',
hidden: true,
meta: {
title: '404'
},
component: () => import(/* webpackChunkName: "400" */ '../views/404.vue')
}
]
- 获取router/modules文件夹中注册个模块路由
const modules = import.meta.glob('./modules/**/*.ts', { eager: true })
let routeModuleList: RouteRecordRaw[] = []
// 获取路由并排序
Object.values(modules).forEach((key: any) => {
const mod = key.default || []
const modList = Array.isArray(mod) ? [...mod] : [mod]
routeModuleList.push(...modList)
})
- 导出挂载在app上的router。
前端固定路由模式会在此把所有路由挂载上。动态路由只需要挂载constanctRoutes,动态路由在前置守卫中挂载。
let routes = constantRoutes
// 前端固定路由模式
if (import.meta.env.VITE_PERMISSION_MODE === 'CONSTANT') {
routes = [...routeModuleList, ...constantRoutes]
}
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
export { constantRoutes, routeModuleList, lastRoutes }
除了router文件夹中的文件,我们还需要:
- useUserStore 管理用户状态,判断路由权限时会用到它,侧边栏菜单的路由表也暂时放在这里。
- 两个API:获取用户信息、获取路由信息
通用判断
当一个导航触发时,我们通过前置守卫来处理各种情况下的导航——判断是否放行,或是跳转其他页面。
代码放在router/permission.ts,如果你习惯放在src下也没问题。
-
如果用户未登录,
当前路由在白名单则直接放行,否则跳转到登录页面/login
-
如果用户已登录,导航到/login,则直接进入系统首页
/
// 用户已登录
if (useUser.userid) {
if (to.path === '/login') {
return '/'
}
...
} else {
// 白名单,直接放行
if (whiteList.indexOf(to.path) > -1) return true
// 非白名单,去登录
else return '/login'
}
继续往下走,需要用到开始说的3种路由模式分别处理。
1. 前端固定路由
我们扩展了_RouteRecordBase
接口,增加roles用于保存可访问路由的角色
declare module 'vue-router' {
interface _RouteRecordBase {
hidden?: boolean
}
interface RouteMeta {
order?: number
roles?: array
}
}
router/modules/system.ts
...
{
path: 'account',
name: 'account',
meta: {
title: 'accountManagement',
roles: [RoleEnum.USER] // 用于判断角色权限
},
component: () => import('~/views/system/account/index.vue')
},
...
在登录状态下,只需要判断用户角色是否能够访问这个路由即可,如果没有权限则跳转去403页面
// 前端固定路由模式,如果没有权限,进入403页面
if (
import.meta.env.VITE_PERMISSIOIN_MODE === 'CONSTANT' &&
to.meta.roles &&
!to.meta.roles.includes(role)
) {
return '/403'
}
前端动态路由模式
无论是前端还是后端动态路由模式,都需要动态挂载。
// 前端固定路由模式,如果没有权限,进入403页面
if (
...
}
// 前端动态路由和后端动态路由,动态挂载路由
else {
if (!to.redirectedFrom) {
await addAsyncRoutes(router, useUser)
return { ...to, replace: true }
} else return true
}
前端模式中,通过路由角色权限和当前用户角色比对,筛选出有权限的路由,挂载到router上
后端模式中,通过接口返回数据,组装成前端可用的路由数据,挂载到router上
动态挂载路由: router.addRoute(route)
在此,因为addRoute不会改变router.optioins.routes(创建路由器时的原始routes),所以挂载后的路由数据,我们需要保存起来,以便侧边栏菜单使用。
async function addAsyncRoutes(router, userStore) {
const permissionMode = import.meta.env.VITE_PERMISSIOIN_MODE
let filteredRoutes = <RouteRecordRaw[]>[]
// 前端动态路由模式
if (permissionMode === 'FRONT') {
filteredRoutes = filterFunc(routeModuleList)
}
// 后端动态路由模式
if (permissionMode === 'BACK') {
let routes = await getBackAsyncRoutes(userStore.userid)
filteredRoutes = formatAsyncRoutes(routes)
}
console.log(filteredRoutes)
filteredRoutes.forEach((val) => router.addRoute(val))
userStore.setAsyncRoutes(filteredRoutes) // 保存最新的路由信息
}
所以前端动态路由,只需过滤出如何角色权限的路由即可,在router/index导出的模块路由数据中比对role即可。
后端动态路由
它和前端模式的区别就是,路由数据通过接口获取,拿到数据后,最重要的操作就是重新拼装成前端可用的形式。
async function getBackAsyncRoutes(userid: number) {
return await systemApi.getRoutes({ userid })
}
const modules = import.meta.glob('~/views/**/**.vue')
// 把接口路由组装成前端可用结构
function formatAsyncRoutes(routes: any[]) {
routes.forEach((r) => {
if (r.component === 'layout' || !r.component) {
r.component = Layout
} else {
r.component = modules[`/src/views${r.component}`]
}
if (r.children && r.children.length > 0) {
r.children = formatAsyncRoutes(r.children)
}
})
return routes
}
产品需求千千万,万变不离其宗 😄
写在最后
路由权限这块,这也是项目最初立下的Flag,在我✍️写到系统第18篇文章的时候终于写了,刚开始的时候还是碰到一些问题,但是当我细细梳理,画出思维导图,写完文章后,觉得一切都清晰起来。
如果一味前行不作停留,不去回顾和总结,很多东西带着一知半解就过去了忘记了。
在忙碌的项目中,我觉得自己变成了一个螺丝钉,没有丝毫成长。停下来做一些输出和总结,整装待发反而更好。
项目地址
本项目GIT地址:https://github.com/lucidity99/mocha-vue3-system
如果有帮助,给个star ✨ 点个赞👍