#该方案亲测有效,由于部分字段是我项目中的,使用时根据自己项目的具体字段进行更改就行#
由于我的项目,客户对菜单进行了定制化,导致我的菜单结构的层级有了三级甚至更深,导致使用keep-alive对三级和以上的页面进行缓存时,缓存失效
一、 keep-alive基本介绍**:**
keep-alive包裹路由组件 keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。有以下参数: include - string | RegExp | Array。只有名称匹配的组件会被缓存。 exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。 max - number | string。最多可以缓存多少组件实例。
二、解决办法:
目前有将三级路由转为二级路由的解决方法,但是不适合我的项目,这里就不介绍该方案了;
我这里使用的是 ParentView 包装模式
是一种非常经典的解决 Vue Router 多级嵌套路由缓存问题的方案
方案原理
问题根源 :在多级嵌套路由中,只有最外层的 <keep-alive>能正常工作,内层的路由组件因为层级太深,无法被外层的 <keep-alive>缓存。
解决方案 :在每个需要缓存的嵌套层级中,都用一个名为 ParentView的组件包裹 <router-view>,并且把这个 ParentView组件本身加入到缓存列表中。
实现步骤
1. 创建 ParentView.vue 组件
javascript
<template>
<!-- 这个组件的作用是作为嵌套路由的中转站 -->
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
</template>
<script lang="ts">
export default {
name: 'ParentView' // 这个名称很重要,需要被缓存
};
</script>
<script setup lang="ts">
import { computed } from 'vue';
import { useTagsViewStore } from '@/store/modules/tagsView';
const tagsViewStore = useTagsViewStore();
// 在 include 中自动包含 ParentView
const cachedViews = computed(() => {
return ['ParentView', ...tagsViewStore.cachedViews];
});
</script>
2. 修改 AppMain.vue(主视图容器)
javascript
<script setup lang="ts">
import { useTagsViewStore } from "@/store/modules/tagsView";
const tagsViewStore = useTagsViewStore();
// 关键:在缓存列表中自动包含 ParentView
const cachedViews = computed(() => {
return ['ParentView', ...tagsViewStore.cachedViews];
});
</script>
<template>
<section class="app-main">
<!-- 默认先去掉面包屑功能 -->
<!-- <breadcrumb /> -->
<!-- <transition name="router-fade" mode="out-in"> -->
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
<!-- </transition> -->
</section>
</template>
<style lang="scss" scoped>
.app-main {
position: relative;
width: 100%;
/* 50= navbar 50 */
min-height: 100vh;
overflow: hidden;
// background-color: var(--el-bg-color-page);
background: #f3f9fd;
}
.fixed-header+.app-main {
padding-top: 40px;
}
.hasTagsView {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
min-height: 100vh;
}
.fixed-header+.app-main {
min-height: 100vh;
padding-top: 40px;
}
}
</style>
3. 修改路由配置
我的路由是动态配置的,从接口获取的,下面是处理菜单的各部分代码
获取数据
javascript
const { accessibleModuleList } = await userStore.getInfo();
//下传前端的菜单权限
const accessRoutes = await permissionStore.generateRoutes(accessibleModuleList);
// console.log('路由数据', accessRoutes, accessibleModuleList)
accessRoutes.forEach((route) => {
router.addRoute(route);
});
重点内容:permissionStore.generateRoutes 菜单处理部分(其中判断是否需要使用的方法,要根据自己的菜单数据修改)
javascript
import { RouteRecordRaw } from "vue-router";
import { defineStore } from "pinia";
import { constantRoutes } from "@/router";
import { store } from "@/store";
const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("@/layout/index.vue");
const Other = () => import("@/layout/other.vue");
const ParentView = () => import("@/layout/components/ParentView.vue");
// 判断是否需要使用 ParentView
const shouldUseParentView = (route: any) => {
if((route.resourceType === 'Page') && route.childrenList && (route.childrenList.length > 0)){
return true;
}else{
return false;
}
};
const filterAsyncRoutes = (routes: [],basepath:string) => {
const asyncRoutes: RouteRecordRaw[] = [];
routes.sort((item1, item2) => {
return item1.sort - item2.sort
}).forEach((route) => {
// console.log('路由数据 ---',route)
// 判断当前层级是否需要使用 ParentView
const useParentView = shouldUseParentView(route);
const tmpRoute = {
// path: basepath+'/'+route.menuUrl,
// component: basepath + '/' + route.menuUrl,
// name: route.menuName,
// meta: { title: route.menuName, icon: route.icon, show: false, keepAlive: true },
path: basepath + '/' + route.resourceUrl,
// component: basepath + '/' + route.resourceUrl,
// 先设置默认值,后面会动态设置
component: null as any,
name: route.resourceName,
meta: { title: route.resourceName, type: route.resourceType, icon: route.icon, show: route.isShow, keepAlive: true, sort: route.sort },
children: route.childrenList.length > 0 ? route.childrenList : []
}; // ES6扩展运算符复制新对象
// 解析组件
if (tmpRoute.path?.toString() == "Layout") {
tmpRoute.component = Layout;
} else if (useParentView) {// 如果有子路由且(三级或以上路由)
tmpRoute.component = ParentView;
} else {// 尝试加载具体组件
const componentPath = `../../views${tmpRoute.path}.vue`;
const component = modules[componentPath];
if (component) {
tmpRoute.component = component;
} else {
// 找不到组件的情况
if (tmpRoute.children?.length > 0) {
tmpRoute.component = Other;
} else {
tmpRoute.component = modules[`../../views/error-page/404.vue`];
}
}
}
// //解析路径(旧的逻辑)
// if (tmpRoute.component?.toString() == "Layout") {
// tmpRoute.component = Layout;
// } else {
// const component = modules[`../../views${tmpRoute.component}.vue`];
// if (component) {
// tmpRoute.component = component;
// } else {
// if(tmpRoute.children.length > 0){
// tmpRoute.component = Other
// }else {
// tmpRoute.component = modules[`../../views/error-page/404.vue`];
// }
// }
// }
if (tmpRoute.children && tmpRoute.children.length > 0) {
tmpRoute.children = filterAsyncRoutes(tmpRoute.children.sort((item1, item2) => {
return item1.sort - item2.sort
// if (item1.sort > item2.sort) {
// return 1;
// } else {
// return -1;
// }
}), basepath + '/' + route.resourceUrl); // basepath + '/' + route.menuUrl
}
asyncRoutes.push(tmpRoute);
});
return asyncRoutes;
};
// setup
export const usePermissionStore = defineStore("permission", () => {
//当前模块的id
const menuid = ref<string>()
//当前路径
const currpath = ref<string>()
// state
const routes = ref<RouteRecordRaw[]>([]);
// actions
function setRoutes(newRoutes: RouteRecordRaw[]) {
routes.value = constantRoutes.concat(newRoutes);
}
function setMenuid(id: string) {
menuid.value = id
}
function setCurrPath(path: string) {
currpath.value = path;
}
/**
* 生成动态路由
*
* @param childrenList 用户的菜单集合
* @returns
*/
function generateRoutes(childrenList: []) {
//构成菜单树形结构
//1.第一级 默认为 头部
//2.第二级 为功能菜单
//拼转成前端数据
const asyncRoutes: RouteRecordRaw[] = [];
childrenList.sort((item1, item2) => {
// if (item1.sort > item2.sort) {
// return 1;
// } else {
// return -1;
// }
return item1.sort - item2.sort
}).map((model) => {
//模块
const tmp = {
path: model.url,
component: Layout,
redirect: model.url,
name: model.moduleName,
meta: { title: model.moduleName, id: model.id, icon: model.icon, show: true, keepAlive: true },
// children: model.accessibleMenuList.length > 0 ? model.accessibleMenuList : []
children: model.childrenList.length > 0 ? model.childrenList : []
}
if (tmp.children && tmp.children.length > 0) {
tmp.children = filterAsyncRoutes(tmp.children, model.url);
}
asyncRoutes.push(tmp);
});
//设置当前模块
// console.log("当前路径的数据:", asyncRoutes)
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
setRoutes(asyncRoutes);
resolve(asyncRoutes);
});
}
return { routes, menuid, currpath, setRoutes, generateRoutes, setMenuid, setCurrPath };
});
// 非setup
export function usePermissionStoreHook() {
return usePermissionStore(store);
}
5. 修改您的需要缓存的页面name,对应上菜单的name即可缓存
javascript
<script lang="ts">
export default {
name: '大货预估' // 这个名称需要与路由配置中的 name 或组件名一致
};
</script>
<script setup lang="ts">
onMounted(() => {
});
onUnmounted(() => {
});
</script>
工作原理图解
bash
Layout
├── AppMain
│ └── <keep-alive :include="['ParentView', '大货预估', ...]">
│ ├── 一级路由组件
│ └── ParentView (缓存了) <-- 这个被缓存了
│ └── <keep-alive :include="['ParentView', '大货预估', ...]">
│ └── 二级/三级路由组件 (大货预估) <-- 这个也被缓存了
为什么这个方案有效?
-
ParentView 自身被缓存 :由于
ParentView在include列表中,它会被<keep-alive>缓存 -
ParentView 内部也有
<keep-alive>:它内部也包裹了一个<keep-alive>,可以缓存它的子组件 -
形成缓存链 :这样无论多少层级,只要中间有
ParentView,整个链条都能被缓存
总结
ParentView 方案是完全可行 的,这是处理 Vue Router 多级嵌套路由缓存的标准做法之一。它的主要优点是:
-
结构清晰:每个嵌套层级都有独立的缓存控制
-
兼容性好:无论多少层级都能正常工作
-
维护简单 :只需在需要缓存的路由层级使用
ParentView组件