前言:路由的权限验证方式为后端返回权限标识字段,前端通过对权限标识字段进行验证,验证通过则显示相应的路由,不通过则不显示。因此将路由分为需要权限验证和不需要权限验证两种。
1、基础准备
(1)创建文件
文件介绍:
router文件夹
- actionRouter.ts 需要权限验证的路由
- index.ts 路由入口
- staticRouter.ts 不需要权限验证的路由 任何人都可以访问
stores
- permissionRouter.ts 验证路由权限相关的状态管理内容
utils文件夹
- router.ts 编写路由守卫
2、路由页面
views/home/index.vue(布局容器)
<template>
<div class="common-layout height100">
<el-container class="height100">
<el-aside width="200px">
<!-- 侧边内容 -->
<sideBar />
</el-aside>
<el-container>
<el-header>
<!-- 标题内容 -->
<headerBar/>
</el-header>
<el-main>
<!-- 内容主体 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
import headerBar from "@/components/headerBar/index.vue";
import sideBar from "@/components/sideBar/index.vue";
</script>
staticRouter.ts文件
export default [
{
path: '/home',
name: 'home',
component:()=>import('@/views/home/index.vue'),//home中包含布局容器,如果是需要布局容器,就在最外层路由中加入这个component,如果不需要布局容器,就像下面的/login一样单独写在外面并在meta中加入 hidden:true
meta:{
index:1,//标识是否为一级菜单
},
children:[
{
path: '/home/userDetails',
component: () => import('@/views/testPool/index.vue'),
meta:{
title:'测试title-1'
},
},
{
path: '/home/abcd',
component: () => import('@/views/testPool/index.vue'),
meta:{
title:'测试title-2'
},
},
],
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
meta:{
hidden:true,// 标识是否在侧边栏菜单中展示
}
},
]
actionRouter.ts 文件
export default [
{
path: '/userlist',
name: 'userlist',
component:()=>import('@/views/home/index.vue'),
meta:{
index:1,
title:'列表'
},
children:[{
path: '/userlist/userlist1-1',
name: 'userlist1-1',
component: () => import('@/views/writerlist/index.vue'),
meta:{
perStr:'userlist页面Str',
title:'写手列表',
},
},{
path: '/userlist/userlist2-2',
name: 'userlist2-2',
component: () => import('@/views/userList/index.vue'),
meta:{
perStr:'userlist页面Str',
title:'用户列表',
},
}
],
},
{
path: '/userDetails',
component: () => import('@/views/testPool/index.vue'),
meta:{
hidden:true,
perStr:'详情页面Str',
},
},
]
index.ts文件
import { createRouter, createWebHistory } from 'vue-router'
import staticRouter from "./staticRouter";
import actionRouter from "./actionRouter";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
// 首先加载无需权限验证的路由
...staticRouter,
{
path:'/',
component:()=>import('@/views/home/index.vue'),
},
]
})
export const perRouter = [
// 将需要验证的路由 以及404页面导出备用
...actionRouter,
{// 匹配之前路由未能匹配到的内容,也就是路由输入错误的情况,这个路由必须写在所有路由最后面
path:'/:path(.*)',
component:() => import('@/views/404/index.vue'),
}
]
export default router
utils/Auth.ts
import Cookies from 'js-cookie'
export const setToken = (token:string)=>{
return Cookies.set('myToken', token, { expires: 1 })
}
export const getToken = ()=>{
return Cookies.get('myToken')
}
export const removeToken = ()=>{
//在存储用户信息时加一个通用前缀,在退出登录时即可通过这种方式批量删除
for (const key in localStorage) {
if(key.includes('xinba-')){
localStorage.removeItem(key)
}
}
return Cookies.remove('myToken')
}
utils/router.ts(路由守卫-重要)
import router from "@/router/index";
import { setToken,getToken} from "@/utils/Auth";
import permissionRouter from '@/stores/permissionRouter'
let noTokenRouter = ['/login']
router.beforeEach(async(to,form,next)=>{
// 如果是白名单路由,直接放行 主要用于一些无需登录页面,例如登录注册
if(noTokenRouter.includes(to.path))next()
// 如果没有token 直接去登录页
let myToken = getToken()
if(!myToken)next({path:'/login'})
// 引入验证权限的状态管理
const myPerStore = permissionRouter()
// perRouterFlag 标识是否已经加载过需要权限验证的路由,false为未加载,就进行验证权限然后添加路由
if(!myPerStore.perRouterFlag){
// addRouteFn() 是状态管理中的 获取有权限的路由的方法 最后返回结果为通过验证的路由
const addRouterList = await myPerStore.addRouteFn()
addRouterList.forEach(item=>{
router.addRoute(item)
})
// 通过next({...to}) 重新跳转到当前路由。如果这一步不重新跳转,就会导致在新添加的路由刷新页面时页面会为空
next({...to})
}else{
// 如果已经加载过路由,就正常跳转
next()
}
})
export default router
stores/permissionRouter.ts(权限状态管理-核心)
import { ref, computed, reactive } from 'vue'
import { defineStore } from 'pinia'
import {perRouter} from '@/router/index' //引入需要权限验证的路由
import type { RouteRecordRaw } from 'vue-router'
// 验证权限的方法 参数为 权限标识数组,路由数组
function filterRoute(permissionArr:string[],routerList:RouteRecordRaw[]){
let newRouter:RouteRecordRaw[] = reactive([])
routerList.forEach((item:RouteRecordRaw)=>{
// 如果不包含 perStr ,说明不需要权限验证,可以直接显示
// 如果包含,就验证改perStr是否在返回的权限列表里
if(!item?.meta?.perStr||permissionArr.includes(item.meta.perStr as string)){
if(item.children){
// 如果有child,就递归调用
item.children = filterRoute(permissionArr,item.children)
}
newRouter.push(item)
}
})
return newRouter
}
export default defineStore('permission', () => {
let permissionArr:string[] = reactive([]) // 存放权限标识的数组
let perRouterFlag = ref(false); //路由是否加载完成
// 验证路由权限的方法
async function addRouteFn(){
// 获取本地储存的权限标识,如果没有就调接口获取
let catchUserPermission = JSON.parse(localStorage.getItem('xinba-userPermission')||"[]")
if(catchUserPermission.length>0){
catchUserPermission.forEach((item:string)=>{
permissionArr.push(item)
})
perRouterFlag.value = true;//获取完成之后将这个值设置为 true,表示路由已经获取完成了
}else{
//调接口获取权限列表
['userlist页面Str'].forEach((item:string)=>{
permissionArr.push(item)
})
perRouterFlag.value = true;//获取完成之后将这个值设置为 true,表示路由已经获取完成了
localStorage.setItem('xinba-userPermission',JSON.stringify(permissionArr))
}
return filterRoute(permissionArr,perRouter)
}
return { addRouteFn,perRouterFlag }
})
3、侧边栏效果实现
components/sideBar/checkPermission.ts
import router from '@/router';
import type { RouteRecordRaw } from 'vue-router'
type RouArr = RouteRecordRaw[]
// 过滤掉路由中的meta:{hidden:true}
function filterRouter(router:RouArr):RouArr{
return router.filter((item:RouteRecordRaw)=>{
// 如果hidden为true,返回false,过滤掉
if(item.meta?.hidden)return false
// 如果有child,继续递归调用
if(item.children&&item.children.length>0){
item.children = filterRouter(item.children)
return {...item}
}else{
return [item]
}
})
}
const allRouter = router.getRoutes()
export const routerArr = filterRouter(allRouter.filter(item=>item.meta.index==1))
components/sideBar/index.vue
<template>
<div class="side-box">
<el-menu
:default-active="defaultActive"
class="el-menu-vertical-demo"
router
>
<template v-for="(route,index) in routerArr" :key="index">
<el-sub-menu v-if="route.children&&route.children.length>0" :index="route.path">
<template #title>
<el-icon><location /></el-icon>
<span>{{ route.name }}</span>
</template>
<el-menu-item v-for="(item,i) in route.children" :key="i" :index="item.path">{{ item.meta?.title||'未知标题' }}</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="route.path">
<el-icon><Menu /></el-icon>
<span>{{ route.name }}</span>
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import { routerArr } from "./checkPermission";
const myUseRoute = useRoute()
const defaultActive = computed(()=>{
return myUseRoute.path
})
</script>
<style lang="scss" scoped>
.side-box{
height: 100%;
.el-menu{
height: 100%;
}
}
</style>