注意,此解决办法只适用于顶级路由为布局组件,中层路由只为嵌套、末级路由调用页面组件的路由结构
PS: 如您无暇查看解决过程,可直接点击右侧目录中的 步骤一步骤二 参考修改即可

问题起因

最近公司项目开发,为了套个好看的外壳加上一些基础的功能,选用了花裤衩大佬的 vue-element-admin 项目,移除了一些不需要的功能,最终剩下了后台布局、tab页签、登录页、mock等,基于此开始了愉快的项目开发。

按原型图要求,有如下页面结构:

  • 平台管理(侧边栏目录)
    • 厂商管理(侧边栏菜单)
      • 厂商列表(菜单点击后跳转的首页)
      • 新增厂商(点新增按钮跳转到此页面)
      • 编辑厂商(点编辑按钮跳转到此页面)

之后脑子一抽,将路由表定义成了如下结构(后来细想之后,其实完全没有必要搞成三层结构 - -):

export const plantformRouter = {
    path: '/plantform',
    component: Layout,
    name: 'Plantform',
    redirect: 'noRedirect',
    meta: {
        title: "平台管理",
        icon: 'plantform'
    },
    children: [
        {
            path: 'dept/merchant',
            redirect: '/plantform/dept/merchant/index',
            name: 'PlantformMerchant',
            component: Blank,
            meta: {
                title: "厂商管理"
            },
            children: [{
                path: "index",
                name: "PlantformMerchantIndex",
                hidden: true,
                nodeType: "function",
                component: () => import('@/views/plantform/dept/merchant/index'),
                meta: {
                    title: "厂商管理",
                    breadcrumb: false,
                    activeMenu: '/plantform/dept/merchant'
                }
            }, {
                path: "add",
                name: "PlantformMerchantAdd",
                hidden: true,
                nodeType: "function",
                component: () => import('@/views/plantform/dept/merchant/add'),
                meta: {
                    title: "新增厂商",
                    breadcrumb: false,
                    activeMenu: '/plantform/dept/merchant'
                }
            }, {
                path: "edit",
                name: "PlantformMerchantEdit",
                hidden: true,
                nodeType: "function",
                component: () => import('@/views/plantform/dept/merchant/edit'),
                meta: {
                    title: "编辑厂商",
                    breadcrumb: false,
                    activeMenu: '/plantform/dept/merchant'
                }
            }]
        }
    ]
}

这里还使用到了一个Blank空白容器组件,如下:

<template>
  <router-view />
</template>

<script>
export default {
  name: "Blank",
};
</script>

项目开发渐渐接近尾声,但是项目中所有的页面都无法缓存
由于之前自己也动手编写过tab页签的实现,虽说稀烂,但是依稀记得当时编写的tab页签是能够正常缓存的,当时所使用的技术点:
keep-alive + include
于是接下来开始分析问题现象 + 面向百度解决问题 - -

问题现象

  • 首先,我们看下vue-element-admin在页签缓存上的表现:

    • 如下图,我打开了两个tab页,可以看到,在拖拽select页面,有三个选中项:
      在这里插入图片描述
    • 此时,我删除拖拽select页面的两个选中项:
      在这里插入图片描述
    • 当我切换到“富文本编辑”页面后,再切换回拖拽select页面时,发现拖拽select页面保存了我刚刚的操作:
      在这里插入图片描述
  • 然后,我们看下我现有项目在页签缓存上的表现:

    • 如下图,我打开了两个tab页,可以看到,在“新增厂商”页面,没有输入任何内容:
      在这里插入图片描述
    • 此时,我在“新增厂商”页面输入一些内容:
      在这里插入图片描述
    • 当我切换到“厂商列表”页面,然后再切换回“新增厂商”页面时,发现“新增厂商”页面丢失了我刚刚输入的内容:
      在这里插入图片描述
      至此,问题已基本确定,vue-element-admin并无任何问题,是由于现有项目上的一些稀奇古怪的操作导致了tab页签不缓存问题的发生,接下来,我们来解决这个问题

解决过程

首先,我从百度搜索找到了如下的解决方案:
element-admin嵌套路由页面不能缓存问题解决
vue element admin 三级路由缓存问题的解决办法

我并未采用如上方案处理,各位看官可以参考下如上方案,后来找到了如下博文:
vue多级菜单(路由)导致缓存(keep-alive)失效

参考如上几篇博文,已基本确定问题根源,其原因在于vue-router无法缓存超过两级的路由,上面的博文也给出了相应的解决方案,由于与项目中实际代码有点不一致,接下来我自己按照如上博文的思路对原有代码进行了相应的修改。

首先,vue-element-admin将路由表维护在前端项目中,该路由表遵从树状结构(为了渲染侧边栏菜单树),那么为了满足vue-router最大只支持2级路由缓存的要求,我们需要将权限过滤完成的路由进行扁平化处理,先梳理下原项目相关代码:

  1. vue-router钩子函数定义(src/permission.js):
// 只摘取核心代码
router.beforeEach(async (to, from, next) => {
	// 拉取用户权限数据
    const { permissions } = await store.dispatch('user/getInfo')
	
	// 按角色生成对应的路由表
    const accessRoutes = await store.dispatch('permission/generateRoutes', permissions)

	// 动态添加可访问的路由
    router.addRoutes(accessRoutes)
})

如上JS告诉了我们,在哪里生成的路由表,我们需要做的就是将 accessRoutes 做扁平化处理,拍成两级路由

  1. 按角色动态生成路由(src/store/modules/permission.js):
// 只摘取核心代码
const actions = {
  generateRoutes({
    commit
  }, permissionKeys) {
    return new Promise(resolve => {
      // 按菜单key数组过滤权限数据(原项目是按角色过滤权限,这里之前稍微修改了下)
      let accessedRoutes = filterAsyncRoutes(asyncRoutes, permissionKeys, false)

      commit('SET_ROUTES', accessedRoutes)
      // 返回可访问的动态路由,供vue-router beforeEach使用
      resolve(accessedRoutes)
    })
  }
}

如上JS中,我们不需要修改原有的 filterAsyncRoutes 方法,该方法用于过滤出可访问的路由,我们需要做的是将该方法的返回值 accessedRoutes 拍平,然后将resolve方法返回的可访问动态路由替换为我们拍平后的路由数据

  1. 然后,我们看下原项目中路由缓存相关部分的代码:
<!-- src/layout/components/AppMain.vue -->
<template>
  <section class="app-main">
    <keep-alive :include="cachedViews">
      <router-view :key="key" />
    </keep-alive>
  </section>
</template>

<script>
export default {
  name: 'AppMain',
  computed: {
    cachedViews() {
      return this.$store.state.tagsView.cachedViews
    },
    key() {
      return this.$route.path
    }
  }
}
</script>

<!-- src/store/modules/tagsView.js -->
<script>
const state = {
  visitedViews: [],
  cachedViews: []
}

const mutations = {
  ADD_CACHED_VIEW: (state, view) => {
    if (state.cachedViews.includes(view.name)) return
    if (!view.meta.noCache) {
      state.cachedViews.push(view.name)
    }
  }
}
</script>

如上代码中,AppMain组件使用了vuex中的cachedViews作为缓存条件,只有被cachedViews所包含的key对应的组件才会被缓存(这里有一个坑,稍后会说)

解决步骤一:

好了,到这里,我们就基本上知道该怎么修改了,接下来开始愉快的修改之路:

  1. 增加路由扁平化处理方法(src/store/modules/permission.js):

注意: 非完整JS文件,请勿直接拷贝覆盖原文件,请阅读后自行对该文件进行修改
注意: 父级路由不允许包含页面组件,所有页面组件都在末级路由中声明,顶级路由组件是Layout(PS: 您创建的其他布局组件也可以)

/**
 * 生成扁平化机构路由(仅两级结构)
 * @param {允许访问的路由Tree} accessRoutes
 * 路由基本机构: 
 * {
 *   name: String,
 *   path: String,
 *   component: Component,
 *   redirect: String,
 *   children: [
 *   ]
 * }
 */
function generateFlatRoutes(accessRoutes) {
  let flatRoutes = [];

  for (let item of accessRoutes) {
    let childrenFflatRoutes = [];
    if (item.children && item.children.length > 0) {
      childrenFflatRoutes = castToFlatRoute(item.children, "");
    }

    // 一级路由是布局路由,需要处理的只是其子路由数据
    flatRoutes.push({
      name: item.name,
      path: item.path,
      component: item.component,
      redirect: item.redirect,
      children: childrenFflatRoutes
    });
  }

  return flatRoutes;
}

/**
 * 将子路由转换为扁平化路由数组(仅一级)
 * @param {待转换的子路由数组} routes
 * @param {父级路由路径} parentPath
 */
function castToFlatRoute(routes, parentPath, flatRoutes = []) {
  for (let item of routes) {
    if (item.children && item.children.length > 0) {
      if (item.redirect && item.redirect !== 'noRedirect') {
        flatRoutes.push({
          name: item.name,
          path: (parentPath + "/" + item.path).substring(1),
          redirect: item.redirect,
          meta: item.meta
        });
      }
      castToFlatRoute(item.children, parentPath + "/" + item.path, flatRoutes);
    } else {
      flatRoutes.push({
        name: item.name,
        path: (parentPath + "/" + item.path).substring(1),
        component: item.component,
        meta: item.meta
      });
    }
  }

  return flatRoutes;
}

// 在原有的生成可访问路由方法中,增加路由扁平化处理,并返回扁平化处理后的结果,具体修改参看分割线部分:
const actions = {
  generateRoutes({
    commit
  }, permissionKeys) {
    return new Promise(resolve => {
      let accessedRoutes = filterAsyncRoutes(asyncRoutes, permissionKeys, false)
      // -------------------------- 分隔线 ----------------------------
      let flatRoutes = generateFlatRoutes(accessedRoutes)

      commit('SET_ROUTES', accessedRoutes)
      resolve(flatRoutes)
      // -------------------------- /分隔线 ----------------------------
    })
  }
}
  1. 为了方便后续维护,还是修改下vue-router钩子函数的变量名称吧(PS: 不改也行):
// 拉取用户权限数据
const { permissions } = await store.dispatch('user/getInfo')

// 这里把原来的 accessRoutes 修改成了 flatRoutes
const flatRoutes = await store.dispatch('permission/generateRoutes', permissions)

// dynamically add accessible routes
router.addRoutes(flatRoutes)

解决问题的看官,请直接跳到步骤二


到这里,我们已经完成了基本的改造,按道理来说路由缓存应该就生效了,然而实际情况并非如此,再次测试发现路由组件还是一如既往地重新渲染。
然后就开始了 百度找答案比对原有项目相关代码安装vue-devtools观察 之路,最终有如下发现:

  • 现有项目表现:
    在这里插入图片描述
  • 原项目表现:
    在这里插入图片描述
    emmm,好像跟原项目的表现不太一样,为啥原项目的标签叫“Dashboard”,而现有项目的标签叫“Index”,到这里,问题原因大概也能猜出来了,路由缓存是基于name的,这个name指的不是路由配置中的name属性值,而是组件的name属性值(PS:这个搞的时间有点长,我一直以为是按路由的name属性匹配的,尝试多次之后才发现是按组件的name值匹配的)
    参考博文: vue.js的keep-alive include无效

解决步骤二:

为所有相关页面的组件,添加name属性值,注意: name属性值必须与路由表所声明的name属性值保持一致。
现有项目路由组件修改示例(可以参看文章开头的路由表配置,这里的组件名称与路由表name属性一一对应):

<script>
export default {
  name: "PlantformMerchantAdd"
}
</script>

<script>
export default {
  name: "PlantformMerchantEdit"
}
</script>

<script>
export default {
  name: "PlantformMerchantIndex"
}
</script>

上一张处理完成的结果图:

在这里插入图片描述

总结

本篇知识点:

  1. vue-router只会缓存1、2级路由,超出2级则不会缓存
  2. keep-alive 的 include属性会匹配对应组件的名称,匹配上的才会缓存
  3. vue-router的name属性是用于路由跳转,与keep-alive的include无任何关系
Logo

前往低代码交流专区

更多推荐