feat: 支持三级及其以上路由的 keep-alive 缓存 (#93)

This commit is contained in:
ClariS 2023-08-07 15:14:15 +08:00 committed by GitHub
parent 45b0bea731
commit ad9ff59a40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 176 additions and 35 deletions

View File

@ -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
View 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)
}
}
})
}

View File

@ -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
}) })
/** 重置路由 */ /** 重置路由 */

View File

@ -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) => {

View File

@ -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 }

View File

@ -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) => {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>