本文是从用户登录到动态路由权限控制的全流程梳理。
1、用户登录
我们使用pinia做状态管理,新建userStore.ts文件,用于处理用户登录、登出和储存用户信息。便于其他组件使用用户数据和方法。
主要定义两个方法,登录和登出。登录方法中,登录接口调用成功后,将用户名、token等信息存入store;登出方法,调用登出接口并清除store中的token。
代码如下。
import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
import loginServer from '@/api/loginServer'
export const userStore = defineStore('user', () => {
const token = useLocalStorage('token', '')
const role = useLocalStorage('role', '')
const userName = useLocalStorage('userName', '')
const password = useLocalStorage('password', '')
const isRemember = useLocalStorage('remember', false)
const login = (param: any) => {
return new Promise<void>((resolve, reject) => {
loginServer
.login({
username: param.username,
password: param.password
})
.then((res) => {
token.value = res.token
userName.value = res.userName
role.value = res.role
isRemember.value = param.isRemember
password.value = ''
if (param.isRemember) {
//缓存密码
password.value = param.password
}
resolve()
})
.catch((error) => {
reject(error)
})
})
}
const logout = () => {
return new Promise((resolve, reject) => {
loginServer
.logout()
.then((res) => {
// 清除token
token.value = ''
resolve(res)
})
.catch((error) => {
reject(error)
})
})
}
return {
token,
userName,
password,
isRemember,
role,
login,
logout
}
})
用户输入用户名密码后,点击登录按钮,调用userStore的login方法,接口返回值存入userStore。在登录组件中使用userStore的代码如下。
import { userStore } from '@/store/userStore'
const userStore = userStore()
const login = async (values: any) => {
login_loading.value = true
try {
await userStore.login({
...values
})
router.push('/')
} catch (error) {
}
}
2、路由守卫
登录后进入到路由阶段。
2.1 动态引入组件
这里使用的是动态引入组件,根据菜单数据中配置好的component字段,其值为组件的路径字符串,从而实现动态加载组件。
首先加载views目录下所有vue文件,编写一个根据路径字符串转换为组件的方法,接收菜单数据参数。
// 使用 import.meta.glob 动态加载所有以.vue结尾的文件
const requireComponent = import.meta.glob(['@/views/**/*.vue'])
// 根据路径字符串转换为组件的方法
const convertPathToComponent = (limitMenus: any) => {
if (limitMenus && limitMenus.length > 0) {
limitMenus.forEach((item: any) => { // 遍历菜单数据
const componentPath = item.component // 菜单的component字段值为该组件的路径字符串
item.component = requireComponent[`/src/views/${componentPath}/index.vue`]
if (item.children && item.children.length > 0) {
convertPathToComponent(item.children) // 递归转换
}
})
}
}
菜单数据示例:
let menus = [
{
"menuId": "1",
"meta": {
"hasChildren": true,
"icon": "system",
"menuId": 1,
"title": "系统配置"
},
"path": "system",
"component": null,
"children": [
{
"menuId": "2",
"meta": {
"hasChildren": false,
"icon": null,
"menuId": 2,
"title": "配置列表"
},
"path": "setList",
"component": "system/setList"
},
{
"menuId": "3",
"meta": {
"hasChildren": false,
"icon": null,
"menuId": 3,
"title": "配置文件"
},
"path": "files",
"component": "system/files"
}
]
}
]
2.2 路由守卫逻辑
1)判断目标是否为登录页,是,就移除动态路由,清除权限,并放行;如果目标不是登录页,到2)
2)判断用户已经登录,可以通过token存储状态判断是否登录,是就到3);否就跳到登录页
3)根据menuID获取按钮权限,存入menuStore中
4)判断menuStore中是否有菜单权限数据,是,就放行;否,进入5)
5)获取菜单权限,存储在menuStore,并处理数据,转换组件,并设置重定向到首页
路由守卫中的判断流程图如下图所示。
route.ts关键代码如下:
const requireComponent = import.meta.glob(['@/views/**/*.vue'])
const loginPath = '/login'
const dynamicRoutes: RouteRecordRaw = {
path: '/',
name: 'root',
component: Layout,
children: []
}
let removeRoute: () => void
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: loginPath,
component: Login
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/notFound/index.vue')
}
]
})
router.beforeEach(async (to, from, next) => {
const userStore = userStore()
const menuStore = menuStore()
if (to.path === loginPath) {
removeRoute && removeRoute()
menuStore.limitMenus = []
next()
} else {
if (userStore.token) {
//已登录
const res = await getBtnLimit(to.meta.menuId)
res.forEach((item) => {
menuStore.btnPermissions.add(item)
})
if (menuStore.limitMenus && menuStore.limitMenus.length > 0) {
next()
} else {
try {
const res = await loginServer.getMenus()
userStore.role = res.role
if (res.menus && res.menus.length > 0) {
res.menus.forEach((item) => {
if (item.children && item.children.length > 0) {
const subItem = item.children[0]
item.redirect = item.path + '/' + subItem.path
}
})
//转换组件
convertPathToComponent(res.menus)
menuStore.limitMenus = [
...res.menus
]
dynamicRoutes.children = menuStore.limitMenus
//重定向第一个路由
const firstMenu = menuStore.limitMenus[0]
dynamicRoutes.redirect = firstMenu.path
removeRoute = router.addRoute(dynamicRoutes)
next({ ...to, replace: true })
} else {
menuStore.limitMenus = []
next({
path: loginPath
})
}
} catch (error) {
//权限获取失败跳转到登录页
next({
path: loginPath
})
}
}
} else {
//未登录
next({
path: loginPath
})
}
}
})
// 根据路径字符串转换为组件的方法
const convertPathToComponent = (limitMenus: any) => {
if (limitMenus && limitMenus.length > 0) {
limitMenus.forEach((item: any) => { // 遍历菜单数据
const componentPath = item.component // 菜单的component字段值为该组件的路径字符串
item.component = requireComponent[`/src/views/${componentPath}/index.vue`]
if (item.children && item.children.length > 0) {
convertPathToComponent(item.children) // 递归转换
}
})
}
}