关于 vue3 + vite + ts + pinia 解决权限问题方案
1.目标
使用 pinia 保存服务器返回的权限列表生成路由列表数据和左侧导航列表数据
2.问题描述
在使用 vue3 + pinia 实现此功能时,遇到了很多坑,如:
1.登录后获取权限添加到路由,但还未添加好路由时实际上已经出发了 next(),就跳转到 404 页面
2.在 router.ts 中使用 pinia,pinia 报错,后面查了才知道是路由初始化时 pinia 还未完成初始化
3.路由请求成功并保存在本地后刷新页面 页面路由丢失然后就 404,是因为刷新后路由不会保存,需要重新拿到本地保存的权限列表添加到路由然后再next() ,但此处不能直接 next(),如果直接next(),还是会执行到 404,因为当你刷新时进入到404,此时的 to.path 就是 404,就算你添加了路由,
next()还是会进入 404,所以就需要保存除404以外最后一次跳转的路径,即代码中的 next({ path: menuStore.currentPath })
4.登录后如果权限成功设置,如果此时我们退出登录并且不刷新页面,那么路由就会保存在本地,当我们换一个权限不同的用户登录时,路由里面则会添加个用户的权限,就可以通过 url 访问别人的权限,那么我们不可能强制刷新,这样会影响用户体验,所以只能静态删除权限,退出时遍历本地权限列表,删除路由中的权限 router.removeRoute(‘此处是权限的name’)
以下是我的解决方案
router.ts
import "element-plus/theme-chalk/el-notification.css"
import { ElNotification } from "element-plus"
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"
import { useMenuStore } from '@/store/modules/menu'
import {
getAllPremission,
getRouterList
} from "@/utils/tools"
import errorRouter from './modules/error'
const LOGIN_NAME = '/login'
/* 导入路由文件模板 */
export let routerArray: RouteRecordRaw[] = errorRouter
// export let routerArray: RouteRecordRaw[] = getAllPremission()
const routes: Array<RouteRecordRaw> = [
{
path: '/login',
name: 'login',
component: () => import('@/view/login/index.vue')
},
{ path: '/', redirect: { name: 'login' } },
...routerArray,
{
// 找不到路由重定向到404页面
name: "NotFont",
path: "/:pathMatch(.*)",
redirect: { name: "404" }
}
]
const router = createRouter({
history: createWebHashHistory(),
routes,
strict: false,
// 切换页面,滚动到最顶部
scrollBehavior: () => ({ left: 0, top: 0 })
})
let initAddRoute = true // 是否是初始化添加路由
// 路由拦截
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem('token')
if (to.path === LOGIN_NAME) {
// 此处添加初始化字段重置,因为如果退出后不刷新然后登录不同的用户,权限不一样,会直接添加进路由,这样新的用户
// 就可以通过 url 访问前一个用户的权限,所以退出时必须清空路由,此处重置字段用于重新获取权限数据
initAddRoute = true
next()
} else {
if (!token) {
ElNotification({
title: '登录提示',
duration: 2000,
message: '登录过期,请重新登录111!',
type: 'warning',
})
next(LOGIN_NAME)
} else {
const menuStore = useMenuStore()
// 如果当前跳转的不是 404 页面,则保存要跳转的页面名称
if (to.path !== "/404") {
menuStore.setCurrentPath(to.path)
}
// 获取保存的路由菜单并生成符合路由数据结构的列表的
let routeList: Menu.MenuOptions[] = getRouterList(menuStore.routeList)
// 登录时获取权限
if (initAddRoute && from.name === "login") {
let list = await menuStore.getPermiList()// 获取权限
routeList = await getRouterList(list)// 格式化权限
}
// 判断当前是否是 初始化/刷新 =》添加路由,如果是并且本地保存了路由菜单,则添加进路由
if (initAddRoute && routeList.length > 0) {
routeList.forEach((item: any) => {
router.addRoute(item)
})
initAddRoute = false
// 初次进入时路由还未初始化好,如果直接执行 next(),会跳转到本地的路由 404,
// 所以登录或刷新时判断当前是登录并且跳转的是 404,则重新触发一次路由守卫,此时路由已初始化完,
// 然后再执行 next()
if (from.name === "login" && to.name === "404") {
next({ path: './configAdmin' })// 重新触发导航守卫并保存路径
} else {
next({ path: menuStore.currentPath })// 重新触发导航守卫一次
}
} else {
next()
}
// next()
}
}
})
export default router
menu.ts
import { defineStore } from "pinia"
import { MenuState } from '../interface'
import piniaPersistConfig from '@/config/piniaPersist'
import {
getPermissionList,
} from '@/api/public'
import {
getMenuList
} from "@/utils/tools"
// useMenuStore
export const useMenuStore = defineStore({
id: "MenuState",
state: (): MenuState => ({
// menu collapse
isCollapse: false,
// routeList
routeList: [],
// menuList
menuList: [],
currentPath: ''
}),
getters: {},
actions: {
setCollapse() {
this.isCollapse = !this.isCollapse
},
setRouteList(routeList: any) {
this.routeList = routeList
},
setMenuList(menuList: any) {
this.menuList = menuList
},
setCurrentPath(currentPath: string) {
this.currentPath = currentPath
},
// 获取权限列表
async getPermiList() {
const res: any = await getPermissionList()
if (res.code === 200) {
this.routeList = res.data
this.menuList = getMenuList(res.data)
}
return res.data
},
},
persist: piniaPersistConfig("MenuState")
})
utils/tools.ts
数据格式化方法,可根据自己业务的数据结构自行更改 :
// 获取本地所有权限列表
export const getAllPremission = () => {
const metaRouters = import.meta.glob("../router/modules/*.ts", { import: 'default', eager: true })
let routerArray: any[] = []
Object.values(metaRouters).forEach((item: any) => {
item.map((val: any) => {
routerArray.push(val)
})
})
return routerArray
}
// 封装 动态获取到的权限 数据结构 => 路由
export const getRouterList = (list: Menu.RequestRouteItem[]) => {
if (!list.length) return []
let routeList: Menu.MenuOptions[] = getAllPremission().map(routeItem => {
routeItem.children?.forEach((childrenItem: any) => {
childrenItem.hasPermission = list.some((listItem: Menu.RequestRouteItem) => {
if (listItem.children.length) {
return listItem.children.some(child => {
return childrenItem.meta!.title == child.permissionName
})
} else {
return childrenItem.meta!.title == listItem.permissionName
}
})
if (childrenItem.meta!.title === '测试模版') {
childrenItem.hasPermission = true
}
})
return routeItem
})
// 生成最终权限路由列表
routeList = routeList.map(item => {
item.children = item.children?.filter(child => {
return child.hasPermission
})
return item
})
return routeList
}