前言
在开发管理后台时,都会存在多个角色登录,登录成功后,不同的角色会展示不同的菜单路由。这就是我们通常所说的动态路由权限,实现路由权限的方案有多种,比较常用的是由前端使用addRoutes(V3版本改成了addRoute)动态挂载路由和服务端返回可访问的路由菜单这两种。上一篇文章讲了纯前端实现路由权限,没看过的可以点击文章链接纯前端实现Vue路由权限。今天主要是基于后端返回路由菜单的基础上,实现路由权限功能。
实现思路
后端返回路由菜单主要是在我们登录之后,后端接口会直接返回当前用户可访问的完整路由菜单,相当于前端基于RBAC模型筛选出了前端可访问的路由列表。
需要注意的是,后端返回的路由菜单是不包括login、404等页面的。前端这边还是需要写一份完整的路由列表,基于后端返回的可访问路由菜单去筛选出需要挂载在router上的路由列表。
代码实现
登录
首先是登录,登录成功后,服务端会返回登录用户可访问的路由菜单userMenus,我们一般会将这些信息保存到Vuex里。
登录方法:
const login = () => {
ruleFormRef.value?.validate((valid: boolean) => {
if (valid) {
store.dispatch('userModule/login', { ...accountForm })
} else {
console.log('error submit!')
}
})
}
Vuex对应异步操作:
async login({ commit }, payload: IRequest) {
// 登录获取token
const { data } = await accountLogin(payload)
commit('SET_TOKEN', data.token)
localCache.setCache('token', data.token)
// 获取用户信息
const userInfo = await getUserInfo(data.id)
commit('SET_USERINFO', userInfo.data)
localCache.setCache('userInfo', userInfo.data)
// 获取菜单
const userMenu = await getUserMenu(userInfo.data.role.id)
commit('SET_USERMENU', userMenu.data)
localCache.setCache('userMenu', userMenu.data)
router.replace('/main/analysis/dashboard')
},
接口返回的路由菜单信息:
路由菜单
可以看到,返回的userMenus是一个数组,包含了图标icon、路由名称name、路由地址、子路由children、路由type等重要信息。前面这些信息主要是用于遍历生成页面左侧的菜单列表,路由type则是用于后面筛选出需要挂载在router上的路由列表。
本地路由列表
前端这边还是需要写一份完整的路由列表,我这里打算在router/index.ts里面写入接口不返回的菜单,如login、404等页面。将接口可能返回的菜单单独放在router/main下面。
router/index.ts:
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/main'
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录'
}
},
{
path: '/main',
name: 'main',
redirect: '/main/analysis/dashboard',
component: () => import('@/views/main/index.vue'),
meta: {
title: '核心技术'
}
},
{
path: '/:pathMatch(.*)*',
name: 'notFound',
component: () => import('@/views/404.vue'),
meta: {
title: '页面找不到~'
}
}
]
router/main下面就是写入所有菜单列表:
单个菜单内容,如dashboard.ts:
const dashboard = () => import('@/views/main/analysis/dashboard/dashboard.vue')
export default {
path: '/main/analysis/dashboard',
name: 'dashboard',
component: dashboard,
meta: {
title: '商品统计'
},
children: []
}
整个router目录:
router目录
接下来,我们就需要根据userMenus去过滤我们写好的router/main下面的路由,也就是接口返回的菜单列表对应一份路由列表,然后将路由列表挂载在router上,这样就能访问路由了。
生成路由
现在我们需要根据userMenus生成对应的路由。
首先我们需要去加载所有的路由,也就是router/main下面的路由文件内容。这里我使用的是require.context方法来加载所有的路由。
这里简单介绍下require.context这个api:
require.context 是webpack的一个api,通过执行require.context()函数,来获取指定的文件夹内的特定文件,在需要多次从同一个文件夹内导入的模块,使用这个函数可以自动导入,不用每个都显示的写import来引入。
require.context(directory,useSubdirectories,regExp) 需要的参数:
- directory:要搜索文件的相对路径
- useSubdirectories:是否查询其子目录
- regExp:匹配基础组件文件名的正则表达式
我们就通过这个api来加载router/main下面的路由。
const routeFiles = require.context('../router/main', true, /.ts/)
我们对routeFiles进行打印:
routeFiles
得到了一个对象,我们需要对这个对象进行遍历拿到文件内容:
routeFiles.keys().forEach((key) => {
const route = require('../router/main' + key.split('.')[1]).default
console.log(route)
allRoutes.push(route)
})
打印得到route
route
这样我们就得到了所有的路由,放在allRoutes里面。
接下来我们需要根据userMenus获取需要添加的routes。
开始我们提到过路由type,这个字段主要是区分菜单下是否还有子菜单,1表示有子菜单,2表示没有子菜单。
接口返回的菜单
我们将allRoutes进行遍历,然后根据path与接口返回的菜单列表userMenus里的path进行比较,如果相同就是匹配到了,那我们就需要这条路由,否则就将这条路由过滤掉。由于allRoutes下的每一项都还可能存在子路由,所以这里我们也需要进行递归筛选。具体的方法如下:
const _recurseGetRoute = (menus: any[]) => {
for (const menu of menus) {
if (menu.type === 2) {
const route = allRoutes.find((route) => route.path === menu.url)
if (route) routes.push(route)
} else {
_recurseGetRoute(menu.children)
}
}
}
最终,routes就是我们得到的userMenus所对应的路由列表。
将生成对应的路由的逻辑整理如下:
import { RouteRecordRaw } from 'vue-router'
export function generateRoutes(userMenus: any[]): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = []
// 1.先去加载默认所有的routes
const allRoutes: RouteRecordRaw[] = []
const routeFiles = require.context('../router/main', true, /.ts/)
routeFiles.keys().forEach((key) => {
const route = require('../router/main' + key.split('.')[1]).default
console.log(route)
allRoutes.push(route)
})
// 2.根据菜单获取需要添加的routes
// userMenus:
// type === 1 -> children -> type === 1
// type === 2 -> url -> route
const _recurseGetRoute = (menus: any[]) => {
for (const menu of menus) {
if (menu.type === 2) {
const route = allRoutes.find((route) => route.path === menu.url)
if (route) routes.push(route)
} else {
_recurseGetRoute(menu.children)
}
}
}
_recurseGetRoute(userMenus)
return routes
}
挂载路由
最后,需要将我们得到的routes挂载er上面。
还是将挂载路由的时机放在全局路由守卫这里,我们在router文件夹下创建一个permission.ts,用于写全局路由守卫相关逻辑:
import router from '@/router'
import { RouteLocationNormalized } from 'vue-router'
import localCache from '@/utils/cache'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import store from '@/store'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login']
const userMenu = store.state.userModule.userMenu
router.beforeEach(
async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: any
) => {
document.title = to.meta.title as string
const token: string = localCache.getCache('token')
NProgress.start()
// 判断该用户是否登录
if (token) {
if (to.path === '/login') {
// 如果登录,并准备进入 login 页面,则重定向到主页
next({ path: '/' })
NProgress.done()
} else {
store.dispatch('routesModule/generateRoutes', { userMenu })
// 确保添加路由已完成
// 设置 replace: true, 因此导航将不会留下历史记录
next({ ...to, replace: true })
}
} else {
// 如果没有 token
if (whiteList.includes(to.path)) {
// 如果在免登录的白名单中,则直接进入
next()
} else {
// 其他没有访问权限的页面将被重定向到登录页面
next('/login')
NProgress.done()
}
}
}
)
router.afterEach(() => {
NProgress.done()
})
routesModule文件下的代码:
// 引入generateRoutes
import { generateRoutes } from '@/utils/generateRoutes'
actions: {
generateRoutes({ commit }, { userMenu }) {
const routes = generateRoutes(userMenu)
// 将routes => router.main.children
routes.forEach((route) => {
router.addRoute('main', route)
})
}
}
这样,完整的路由权限功能就完成了。我们可以看一下页面:
系统界面
总结
相比纯前端实现路由权限,这种基于后端返回路由菜单的方式会显得简单一些。我们不需要经过RBAC去过滤出用户可以访问的路由,而是接口直接返回给了我们。我们只需要将路由菜单对应生成一份路由,然后将路由进行挂载。
到此这篇关于前端配合后端实现Vue路由权限的文章就介绍到这了,更多相关后端实现Vue路由权限内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!