vue3 三级路由无法缓存的终极解决方案

#该方案亲测有效,由于部分字段是我项目中的,使用时根据自己项目的具体字段进行更改就行#

由于我的项目,客户对菜单进行了定制化,导致我的菜单结构的层级有了三级甚至更深,导致使用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', '大货预估', ...]">
│               └── 二级/三级路由组件 (大货预估)  <-- 这个也被缓存了

为什么这个方案有效?

  1. ParentView 自身被缓存 :由于 ParentViewinclude列表中,它会被 <keep-alive>缓存

  2. ParentView 内部也有 <keep-alive> :它内部也包裹了一个 <keep-alive>,可以缓存它的子组件

  3. 形成缓存链 :这样无论多少层级,只要中间有 ParentView,整个链条都能被缓存

总结

ParentView 方案是完全可行 的,这是处理 Vue Router 多级嵌套路由缓存的标准做法之一。它的主要优点是:

  1. 结构清晰:每个嵌套层级都有独立的缓存控制

  2. 兼容性好:无论多少层级都能正常工作

  3. 维护简单 :只需在需要缓存的路由层级使用 ParentView组件

相关推荐
兮动人2 小时前
Google Chrome 142更新引发内网访问危机:原理、影响与全面解决方案
前端·chrome
PAQQ2 小时前
ubuntu22.04 搭建 Opencv & C++ 环境
前端·webpack·node.js
这是个栗子2 小时前
git报错:Reinitialized existing Git repository in ...
前端·git·github
ghfdgbg2 小时前
15. Vue工程化 + ElementPlus
前端·javascript·vue.js
Onlyᝰ2 小时前
前端使用jscpd查项目重复率
前端
古城小栈2 小时前
JS 中有 undefined 和 null 两个空值,还有谁!
javascript·ecmascript
pandarking2 小时前
[CTF]攻防世界:web-unfinish(sql二次注入)
前端·数据库·sql·web安全·ctf
IT_陈寒2 小时前
Java并发编程避坑指南:从volatile到ThreadLocal,8个实战案例解析线程安全核心原理
前端·人工智能·后端
ByteCraze2 小时前
前端性能与监控指标采集系统设计方案
前端