动态路由的实现方式
在vue项目要想实现路由的动态控制,有两种主流的实现方向。
- 基于用户角色判断路由,并且在后端接口进行权限管理
- 基于用户信息单独返回配置的路由信息
对于第一种方法,其优点在于不用后端单独配置路由信息,当用户登陆时可直接根据用户的角色
进行前端的路由控制。缺点在于每个用户都可以看到页面的所有路由信息,此时则需要后端单独对接口设置不同的角色权限。
对于第二种方法,其优点在于每个用户都可以配置自己独有的路由
,根据返回的路由信息,加载相应的路由页面,不会在前端暴露所有的路由信息。缺点在于每个用户的路由配置相较于第一种更为繁琐,并且增加了服务器查找路由的负担。
第一种方法的实现可以参考: vue-admin-template
本文将详细介绍第二种,基于后端的路由实现
实现思路
graph TD
start[开始] --> 登录{用户登录}
登录 --登录成功--> 守卫[全局前置路由守卫]
登录 --登录失败--> endd([结束,回到登陆页面])
守卫 --> token{token存在}
token --token存在--> 获取信息[获取信息]
token --token不存在--> endd
获取信息 --> 响应拦截器{axios响应拦截器}
响应拦截器 --token有效--> 设置信息[设置用户和路由信息]
响应拦截器 --token过期--> 清除信息[清除信息]
清除信息 --> endd
设置信息 --> 添加路由([添加路由])
实现过程
用户登录
首先实现登录逻辑,views/login/index.vue
js
const handleLogin = () => {
loginFormRef.value?.validate((valid: boolean) => {
if (valid) {
loading.value = true
useUserStore()
.login({
userName: loginForm.userName,
password: loginForm.password
})
.then(() => {
ElMessage.success({ message: '登录成功' })
router.push({ path: '/' })
})
.catch(() => {
loginForm.password = ''
})
.finally(() => {
loading.value = false
})
} else {
return false
}
})
}
然后实现用户登录token的获取,store/modules/user.ts
js
/** 登录 */
const login = (loginData: ILoginRequestData) => {
return new Promise((resolve, reject) => {
loginApi({
userName: loginData.userName,
password: loginData.password
})
.then((res) => {
const { data } = res
setToken(data.t)
token.value = data.t
resolve(true)
})
.catch((error) => {
reject(error)
})
})
}
登录成功,进入守卫
路由前置守卫判断token,并添加用户信息和路由,router/permission.ts
js
router.beforeEach(async (to, _from, next) => {
NProgress.start()
document.title = getPageTitle(to.meta.title as string)
const userStore = useUserStoreHook()
const permissionStore = usePermissionStoreHook()
// 判断该用户是否登录
if (getToken()) {
if (to.path === '/login') {
// 如果已经登录,并准备进入 Login 页面,则重定向到主页
next({ path: '/' })
NProgress.done()
} else {
try {
await userStore.getInfo()
// 生成后端返回的动态路由
await userStore.getRoutes()
const asyncRoutes = userStore.asyncRoutes
// 此处忽略,可为空数组
const roles = userStore.roles
permissionStore.setDynamicRoutes(asyncRoutes, roles)
// 将'有访问权限的动态路由' 添加到 Router 中
permissionStore.dynamicRoutes.forEach((route) => {
router.addRoute(route)
})
// 确保添加路由已完成
// 设置 replace: true, 因此导航将不会留下历史记录
next({ ...to, replace: true })
} catch (err: any) {
// 过程中发生任何错误,都直接重置 Token,并重定向到登录页面
userStore.resetToken()
ElMessage.error(err.message || '路由守卫过程发生错误')
next('/login')
NProgress.done()
}
}
}
} else {
// 如果没有 Token
if (whiteList.indexOf(to.path) !== -1) {
// 如果在免登录的白名单中,则直接进入
next()
} else {
// 其他没有访问权限的页面将被重定向到登录页面
next('/login')
NProgress.done()
}
}
})
permissionStore.setDynamicRoutes
方法的实现,store/modules/permission.ts
js
const routes = ref<RouteRecordRaw[]>([])
const dynamicRoutes = ref<RouteRecordRaw[]>([])
// 后端返回路由
const setDynamicRoutes = (asyncRoutes: any[], roles: string[]) => {
const cloneRoutes = JSON.parse(JSON.stringify(asyncRoutes))
// 看下面方法实现
const resRoutes = getDynamicRoute(cloneRoutes)
// 添加额外的路由重定向
const rRoutes = redirectRoutes(roles, resRoutes)
routes.value = constantRoutes.concat(resRoutes)
dynamicRoutes.value = rRoutes
}
getDynamicRoute
方法,主要用于递归后端返回的路由信息,并对组件进行加载,utils/asyncRoute.ts
js
import { RouteRecordRaw } from 'vue-router'
import Layout from '@/layout/index.vue'
const modules = import.meta.glob('../views/**/*.vue')
/**动态路由 */
export const getDynamicRoute = (asyncRoute: any[]): RouteRecordRaw[] => {
const newRoute = asyncRoute.map((item) => {
if (item.component === 'Layout') {
item.component = Layout
} else {
item.component = modules[`../views/${item.component}.vue`]
}
if (item.children && item.children.length > 0) {
item.children = getDynamicRoute(item.children)
}
return item
})
return newRoute
}
redirectRoutes
方法,主要用于对不同角色的用户进行首页的不同重定向
js
const redirectRoutes = (roles: string[], routes: RouteRecordRaw[]): RouteRecordRaw[] => {
if (roles.includes('admin') || roles.includes('operation')) {
return [
{
path: '/',
name: 'Root',
redirect: '/d',
meta: { hidden: true }
},
...routes
]
} else {
return [
{
path: '/',
name: 'Root',
redirect: '/s',
meta: { hidden: true }
},
...routes
]
}
}
拦截器逻辑
这里只展示token失效处理部分
js
// 响应拦截(可根据具体业务作出相应的调整)
service.interceptors.response.use(
(response) => {
// 此处自己写逻辑
},
(error) => {
// Status 是 HTTP 状态码
const status = get(error, 'response.status')
const { data } = error.response.data
switch (status) {
case 400:
// error.message = '请求参数错误'
error.message = data.message
break
case 401:
// Token 过期时,直接退出登录并强制刷新页面(会重定向到登录页)
useUserStoreHook().logout()
location.reload()
break
注意事项
- 退出登陆时,需要重置路由信息
js
/** 重置路由 */
export function resetRouter() {
// 注意:所有动态路由路由必须带有 Name 属性,否则可能会不能完全重置干净
try {
router.getRoutes().forEach((route) => {
const { name } = route
if (name) {
router.hasRoute(name) && router.removeRoute(name)
}
})
} catch (error) {
// 强制刷新浏览器
window.location.reload()
}
}
- 路由信息结构
js
{
path: '/dashboard',
// 后端此处返回字符串
component: Layout,
redirect: '/dashboard/workbench',
name: 'Dashboard',
meta: {
title: '首页',
svgIcon: 'dashboard',
roles: ['admin', 'operation']
},
children: [
{
path: 'workbench',
// 后端返回 'dashboard/workbench/index'
component: () => import('@/views/dashboard/workbench/index.vue'),
name: 'WorkBench',
meta: {
title: '工作台',
svgIcon: 'desktop'
}
},
{
path: 'analysis',
component: () => import('@/views/dashboard/analysis/index.vue'),
name: 'Analysis',
meta: {
title: '分析页',
svgIcon: 'analysis'
}
}
]
},