feat: 支持三级及其以上路由的 keep-alive 缓存 (#93)
This commit is contained in:
parent
45b0bea731
commit
ad9ff59a40
@ -1,21 +1,28 @@
|
|||||||
/** 动态路由配置 */
|
/** 动态路由配置 */
|
||||||
interface AsyncRouteSettings {
|
interface RouteSettings {
|
||||||
/**
|
/**
|
||||||
* 是否开启动态路由功能?
|
* 是否开启动态路由功能?
|
||||||
* 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段)
|
* 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段)
|
||||||
* 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 open: false
|
* 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 async: false
|
||||||
*/
|
*/
|
||||||
open: boolean
|
async: boolean
|
||||||
/** 当动态路由功能关闭时:
|
/** 当动态路由功能关闭时:
|
||||||
* 1. 应该将所有路由都写到常驻路由里面(表明所有登陆的用户能访问的页面都是一样的)
|
* 1. 应该将所有路由都写到常驻路由里面(表明所有登陆的用户能访问的页面都是一样的)
|
||||||
* 2. 系统自动给当前登录用户赋值一个没有任何作用的默认角色
|
* 2. 系统自动给当前登录用户赋值一个没有任何作用的默认角色
|
||||||
*/
|
*/
|
||||||
defaultRoles: Array<string>
|
defaultRoles: Array<string>
|
||||||
|
/**
|
||||||
|
* 是否开启三级及其以上路由缓存功能?
|
||||||
|
* 1. 开启后会进行路由降级(把三级及其以上的路由转化为二级路由)
|
||||||
|
* 2. 由于都会转成二级路由,所以二级及其以上路由有内嵌子路由将会失效
|
||||||
|
*/
|
||||||
|
thirdLevelRouteCache: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const asyncRouteSettings: AsyncRouteSettings = {
|
const routeSettings: RouteSettings = {
|
||||||
open: true,
|
async: true,
|
||||||
defaultRoles: ["DEFAULT_ROLE"]
|
defaultRoles: ["DEFAULT_ROLE"],
|
||||||
|
thirdLevelRouteCache: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export default asyncRouteSettings
|
export default routeSettings
|
69
src/router/helper.ts
Normal file
69
src/router/helper.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
type Router,
|
||||||
|
type RouteRecordNormalized,
|
||||||
|
type RouteRecordRaw,
|
||||||
|
createRouter,
|
||||||
|
createWebHashHistory,
|
||||||
|
createWebHistory
|
||||||
|
} from "vue-router"
|
||||||
|
import { cloneDeep, omit } from "lodash-es"
|
||||||
|
|
||||||
|
/** 路由模式 */
|
||||||
|
export const history =
|
||||||
|
import.meta.env.VITE_ROUTER_HISTORY === "hash"
|
||||||
|
? createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH)
|
||||||
|
: createWebHistory(import.meta.env.VITE_PUBLIC_PATH)
|
||||||
|
|
||||||
|
/** 路由降级(把三级及其以上的路由转化为二级路由) */
|
||||||
|
export const flatMultiLevelRoutes = (routes: RouteRecordRaw[]) => {
|
||||||
|
const routesMirror = cloneDeep(routes)
|
||||||
|
routesMirror.forEach((route) => {
|
||||||
|
// 如果路由是三级及其以上路由,对其进行降级处理
|
||||||
|
isMultipleRoute(route) && promoteRouteLevel(route)
|
||||||
|
})
|
||||||
|
return routesMirror
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断路由层级是否大于 2 */
|
||||||
|
const isMultipleRoute = (route: RouteRecordRaw) => {
|
||||||
|
const children = route.children
|
||||||
|
if (children?.length) {
|
||||||
|
// 只要有一个子路由的 children 长度大于 0,就说明是三级及其以上路由
|
||||||
|
return children.some((child) => child.children?.length)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成二级路由 */
|
||||||
|
const promoteRouteLevel = (route: RouteRecordRaw) => {
|
||||||
|
// 创建 router 实例是为了获取到当前传入的 route 的所有路由信息
|
||||||
|
let router: Router | null = createRouter({
|
||||||
|
history,
|
||||||
|
routes: [route]
|
||||||
|
})
|
||||||
|
const routes = router.getRoutes()
|
||||||
|
// 在 addToChildren 函数中使用上面获取到的路由信息来更新 route 的 children
|
||||||
|
addToChildren(routes, route.children || [], route)
|
||||||
|
router = null
|
||||||
|
// 转为二级路由后,去除所有子路由中的 children
|
||||||
|
route.children = route.children?.map((item) => omit(item, "children") as RouteRecordRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将给定的子路由添加到指定的路由模块中 */
|
||||||
|
const addToChildren = (routes: RouteRecordNormalized[], children: RouteRecordRaw[], routeModule: RouteRecordRaw) => {
|
||||||
|
children.forEach((child) => {
|
||||||
|
const route = routes.find((item) => item.name === child.name)
|
||||||
|
if (route) {
|
||||||
|
// 初始化 routeModule 的 children
|
||||||
|
routeModule.children = routeModule.children || []
|
||||||
|
// 如果 routeModule 的 children 属性中不包含该路由,则将其添加进去
|
||||||
|
if (!routeModule.children.includes(route)) {
|
||||||
|
routeModule.children.push(route)
|
||||||
|
}
|
||||||
|
// 如果该子路由还有自己的子路由,则递归调用此函数将它们也添加进去
|
||||||
|
if (child.children?.length) {
|
||||||
|
addToChildren(routes, child.children, routeModule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
import { type RouteRecordRaw, createRouter, createWebHashHistory, createWebHistory } from "vue-router"
|
import { type RouteRecordRaw, createRouter } from "vue-router"
|
||||||
|
import { history, flatMultiLevelRoutes } from "./helper"
|
||||||
|
import routeSettings from "@/config/route"
|
||||||
|
|
||||||
const Layouts = () => import("@/layouts/index.vue")
|
const Layouts = () => import("@/layouts/index.vue")
|
||||||
|
|
||||||
@ -123,7 +125,7 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
redirect: "/menu/menu1",
|
redirect: "/menu/menu1",
|
||||||
name: "Menu",
|
name: "Menu",
|
||||||
meta: {
|
meta: {
|
||||||
title: "多级菜单",
|
title: "多级路由",
|
||||||
svgIcon: "menu"
|
svgIcon: "menu"
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
@ -141,7 +143,8 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import("@/views/menu/menu1/menu1-1/index.vue"),
|
component: () => import("@/views/menu/menu1/menu1-1/index.vue"),
|
||||||
name: "Menu1-1",
|
name: "Menu1-1",
|
||||||
meta: {
|
meta: {
|
||||||
title: "menu1-1"
|
title: "menu1-1",
|
||||||
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -158,7 +161,8 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import("@/views/menu/menu1/menu1-2/menu1-2-1/index.vue"),
|
component: () => import("@/views/menu/menu1/menu1-2/menu1-2-1/index.vue"),
|
||||||
name: "Menu1-2-1",
|
name: "Menu1-2-1",
|
||||||
meta: {
|
meta: {
|
||||||
title: "menu1-2-1"
|
title: "menu1-2-1",
|
||||||
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -166,7 +170,8 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import("@/views/menu/menu1/menu1-2/menu1-2-2/index.vue"),
|
component: () => import("@/views/menu/menu1/menu1-2/menu1-2-2/index.vue"),
|
||||||
name: "Menu1-2-2",
|
name: "Menu1-2-2",
|
||||||
meta: {
|
meta: {
|
||||||
title: "menu1-2-2"
|
title: "menu1-2-2",
|
||||||
|
keepAlive: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -176,7 +181,8 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import("@/views/menu/menu1/menu1-3/index.vue"),
|
component: () => import("@/views/menu/menu1/menu1-3/index.vue"),
|
||||||
name: "Menu1-3",
|
name: "Menu1-3",
|
||||||
meta: {
|
meta: {
|
||||||
title: "menu1-3"
|
title: "menu1-3",
|
||||||
|
keepAlive: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -186,7 +192,8 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import("@/views/menu/menu2/index.vue"),
|
component: () => import("@/views/menu/menu2/index.vue"),
|
||||||
name: "Menu2",
|
name: "Menu2",
|
||||||
meta: {
|
meta: {
|
||||||
title: "menu2"
|
title: "menu2",
|
||||||
|
keepAlive: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -270,11 +277,8 @@ export const asyncRoutes: RouteRecordRaw[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history:
|
history,
|
||||||
import.meta.env.VITE_ROUTER_HISTORY === "hash"
|
routes: routeSettings.thirdLevelRouteCache ? flatMultiLevelRoutes(constantRoutes) : constantRoutes
|
||||||
? createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH)
|
|
||||||
: createWebHistory(import.meta.env.VITE_PUBLIC_PATH),
|
|
||||||
routes: constantRoutes
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 重置路由 */
|
/** 重置路由 */
|
||||||
|
@ -3,7 +3,7 @@ import { useUserStoreHook } from "@/store/modules/user"
|
|||||||
import { usePermissionStoreHook } from "@/store/modules/permission"
|
import { usePermissionStoreHook } from "@/store/modules/permission"
|
||||||
import { ElMessage } from "element-plus"
|
import { ElMessage } from "element-plus"
|
||||||
import { getToken } from "@/utils/cache/cookies"
|
import { getToken } from "@/utils/cache/cookies"
|
||||||
import asyncRouteSettings from "@/config/async-route"
|
import routeSettings from "@/config/route"
|
||||||
import isWhiteList from "@/config/white-list"
|
import isWhiteList from "@/config/white-list"
|
||||||
import NProgress from "nprogress"
|
import NProgress from "nprogress"
|
||||||
import "nprogress/nprogress.css"
|
import "nprogress/nprogress.css"
|
||||||
@ -24,7 +24,7 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
// 检查用户是否已获得其权限角色
|
// 检查用户是否已获得其权限角色
|
||||||
if (userStore.roles.length === 0) {
|
if (userStore.roles.length === 0) {
|
||||||
try {
|
try {
|
||||||
if (asyncRouteSettings.open) {
|
if (routeSettings.async) {
|
||||||
// 注意:角色必须是一个数组! 例如: ['admin'] 或 ['developer', 'editor']
|
// 注意:角色必须是一个数组! 例如: ['admin'] 或 ['developer', 'editor']
|
||||||
await userStore.getInfo()
|
await userStore.getInfo()
|
||||||
const roles = userStore.roles
|
const roles = userStore.roles
|
||||||
@ -32,8 +32,8 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
permissionStore.setRoutes(roles)
|
permissionStore.setRoutes(roles)
|
||||||
} else {
|
} else {
|
||||||
// 没有开启动态路由功能,则启用默认角色
|
// 没有开启动态路由功能,则启用默认角色
|
||||||
userStore.setRoles(asyncRouteSettings.defaultRoles)
|
userStore.setRoles(routeSettings.defaultRoles)
|
||||||
permissionStore.setRoutes(asyncRouteSettings.defaultRoles)
|
permissionStore.setRoutes(routeSettings.defaultRoles)
|
||||||
}
|
}
|
||||||
// 将'有访问权限的动态路由' 添加到 Router 中
|
// 将'有访问权限的动态路由' 添加到 Router 中
|
||||||
permissionStore.dynamicRoutes.forEach((route) => {
|
permissionStore.dynamicRoutes.forEach((route) => {
|
||||||
|
@ -3,7 +3,8 @@ import store from "@/store"
|
|||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
import { type RouteRecordRaw } from "vue-router"
|
import { type RouteRecordRaw } from "vue-router"
|
||||||
import { constantRoutes, asyncRoutes } from "@/router"
|
import { constantRoutes, asyncRoutes } from "@/router"
|
||||||
import asyncRouteSettings from "@/config/async-route"
|
import { flatMultiLevelRoutes } from "@/router/helper"
|
||||||
|
import routeSettings from "@/config/route"
|
||||||
|
|
||||||
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
|
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
|
||||||
const routeRoles = route.meta?.roles
|
const routeRoles = route.meta?.roles
|
||||||
@ -29,9 +30,9 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
const dynamicRoutes = ref<RouteRecordRaw[]>([])
|
const dynamicRoutes = ref<RouteRecordRaw[]>([])
|
||||||
|
|
||||||
const setRoutes = (roles: string[]) => {
|
const setRoutes = (roles: string[]) => {
|
||||||
const accessedRoutes = asyncRouteSettings.open ? filterAsyncRoutes(asyncRoutes, roles) : asyncRoutes
|
const accessedRoutes = routeSettings.async ? filterAsyncRoutes(asyncRoutes, roles) : asyncRoutes
|
||||||
routes.value = constantRoutes.concat(accessedRoutes)
|
routes.value = constantRoutes.concat(accessedRoutes)
|
||||||
dynamicRoutes.value = accessedRoutes
|
dynamicRoutes.value = routeSettings.thirdLevelRouteCache ? flatMultiLevelRoutes(accessedRoutes) : accessedRoutes
|
||||||
}
|
}
|
||||||
|
|
||||||
return { routes, dynamicRoutes, setRoutes }
|
return { routes, dynamicRoutes, setRoutes }
|
||||||
|
@ -9,7 +9,7 @@ import router, { resetRouter } from "@/router"
|
|||||||
import { loginApi, getUserInfoApi } from "@/api/login"
|
import { loginApi, getUserInfoApi } from "@/api/login"
|
||||||
import { type LoginRequestData } from "@/api/login/types/login"
|
import { type LoginRequestData } from "@/api/login/types/login"
|
||||||
import { type RouteRecordRaw } from "vue-router"
|
import { type RouteRecordRaw } from "vue-router"
|
||||||
import asyncRouteSettings from "@/config/async-route"
|
import routeSettings from "@/config/route"
|
||||||
|
|
||||||
export const useUserStore = defineStore("user", () => {
|
export const useUserStore = defineStore("user", () => {
|
||||||
const token = ref<string>(getToken() || "")
|
const token = ref<string>(getToken() || "")
|
||||||
@ -35,7 +35,7 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
const { data } = await getUserInfoApi()
|
const { data } = await getUserInfoApi()
|
||||||
username.value = data.username
|
username.value = data.username
|
||||||
// 验证返回的 roles 是否为一个非空数组,否则塞入一个没有任何作用的默认角色,防止路由守卫逻辑进入无限循环
|
// 验证返回的 roles 是否为一个非空数组,否则塞入一个没有任何作用的默认角色,防止路由守卫逻辑进入无限循环
|
||||||
roles.value = data.roles?.length > 0 ? data.roles : asyncRouteSettings.defaultRoles
|
roles.value = data.roles?.length > 0 ? data.roles : routeSettings.defaultRoles
|
||||||
}
|
}
|
||||||
/** 切换角色 */
|
/** 切换角色 */
|
||||||
const changeRoles = async (role: string) => {
|
const changeRoles = async (role: string) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-card header="menu 1">
|
<el-card header="二级路由 - menu1">
|
||||||
<router-view />
|
<router-view />
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "Menu1-1"
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = ref("")
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-card> menu 1-1 </el-card>
|
<el-card header="三级路由缓存 - menu1-1">
|
||||||
|
<el-input v-model="text" />
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-card header="menu 1-2">
|
<el-card header="三级路由 - menu1-2">
|
||||||
<router-view />
|
<router-view />
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "Menu1-2-1"
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = ref("")
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-card> menu 1-2-1 </el-card>
|
<el-card header="四级路由缓存 - menu1-2-1">
|
||||||
|
<el-input v-model="text" />
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "Menu1-2-2"
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = ref("")
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-card> menu 1-2-2 </el-card>
|
<el-card header="四级路由缓存 - menu1-2-2">
|
||||||
|
<el-input v-model="text" />
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "Menu1-3"
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = ref("")
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-card> menu 1-3 </el-card>
|
<el-card header="三级路由缓存 - menu1-3">
|
||||||
|
<el-input v-model="text" />
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "Menu2"
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = ref("")
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-card> menu 2 </el-card>
|
<el-card header="二级路由缓存 - menu2">
|
||||||
|
<el-input v-model="text" />
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user