【Fantastic-admin 技术揭秘】关于 KeepAlive 多级路由缓存问题的终极解决方案

715 阅读3分钟

《Fantastic-admin 技术揭秘》系列将带你了解 Fantastic-admin 这款框架各种功能的设计与实现。通过了解这些技术细节,你不光可以更轻松地使用 Fantastic-admin 这款框架,也可以在其他项目中使用这些技术。

你可以点击 这里 查看本系列的所有文章,也欢迎你在评论区留言告诉我你感兴趣的内容,或许下一篇文章就会带你揭秘其中的奥秘。

前言

在后台管理系统中,多级路由缓存一直是个老生常谈的话题,我 4 年前写的《一劳永逸,解决基于 keep-alive 的后台多级路由缓存问题》这篇文章,如今依旧有人点赞收藏。

但随着 vue-router 的更新,如今已经有了更好的解决方案。

分析需求

路由不管配置成两级还是多级,都可以全局控制任意路由的缓存。

原有方案

也就是 4 年前我提供的方案,如果你不想了解,可以跳过这一小节。

首先我们知道 KeepAlive 组件是用于缓存子组件而设计的,而父子组件的嵌套关系,和使用 RouterView 组件来嵌套子路由的场景刚好吻合。

通常我们也是这么使用的:

<script setup lang="ts">
import { useKeepAliveStore } from '@/store/modules/keepAlive';

// keepAliveStore.list 存放需要缓存路由对应组件的组件名
const keepAliveStore = useKeepAliveStore();
</script>

<template>
  <RouterView v-slot="{ Component, route }">
    <KeepAlive :include="keepAliveStore.list">
      <component :is="Component" :key="route.fullPath" />
    </KeepAlive>
  </RouterView>
</template>

但它并不支持多级路由,因为多级路由想要让页面正常显示,每个父级路由都需要配置 KeepAlive 组件。

+------------------------------+
| Layout                       |
|  +------------------------+  |
|  | EmptyLayout            |  |
|  |  +------------------+  |  |
|  |  | Page             |  |  |
|  |  +------------------+  |  |
|  +------------------------+  |
+------------------------------+

就像上面这样,需要在 Layout 组件和 EmptyLayout 组件中都配置 KeepAliveRouterView 组件,才能让页面正常显示。

即便这样,它依旧存在问题,因为如果不缓存 EmptyLayout 组件,则 EmptyLayout 下的页面将无法被缓存;但如果缓存了 EmptyLayout 组件,EmptyLayout 下的所有页面又都会被缓存,无法做单独的控制。

所以我在当时提出了一个方案,既然多级路由无解,两级路由又完全没问题,那就将多级路由结构,全部转换成两级路由结构就好了。

+------------------------------+            +------------------------------+
| Layout                       |            | Layout                       |
|  +------------------------+  |            |  +------------------------+  |
|  | EmptyLayout            |  |  +------>  |  | Page                   |  |
|  |  +------------------+  |  |            |  |                        |  |
|  |  | Page             |  |  |            |  |                        |  |
|  |  +------------------+  |  |            |  |                        |  |
|  +------------------------+  |            |  +------------------------+  |
+------------------------------+            +------------------------------+

从数据结构上来看,就是这样:

// 原始数据
{
  path: '/users',
  meta: {
    title: '用户管理'
  },
  children: [
    {
      path: 'clients',
      meta: {
        title: '客户管理'
      },
      children: [
        {
          path: 'list',
          meta: {
            title: '客户列表'
          }
        },
        {
          path: 'detail',
          meta: {
            title: '客户详情'
          }
        }
      ]
    }
  ]
}

// 处理后数据
{
  path: '/users',
  meta: {
    title: '用户管理'
  },
  children: [
    {
      path: 'clients/list',
      meta: {
        title: '客户列表',
        breadcrumb: [
          { path: '/users', title: '用户管理' },
          { path: '/users/clients', title: '客户管理' },
          { path: '/users/clients/list', title: '客户列表' }
        ]
      }
    },
    {
      path: 'clients/detail',
      meta: {
        title: '客户详情',
        breadcrumb: [
          { path: '/users', title: '用户管理' },
          { path: '/users/clients', title: '客户管理' },
          { path: '/users/clients/detail', title: '客户详情' }
        ]
      }
    }
  ]
}

这部处理并不难,通过递归遍历路由数据就可以实现。

需要额外注意的是,因为数据结构变了,原先多级路由可以通过 $route.matched 实现面包屑导航的功能,现在需要手动增加一个 breadcrumb 字段来实现。

最新方案

简单来说,最新方案无需做任何处理,因为 vue-router 4.1 版本提供了一个新特性:忽略父组件

具体使用方法如下:

// 原始数据
{
  path: '/users',
  component: () => import('@/Layout.vue'),
  meta: {
    title: '用户管理'
  },
  children: [
    {
      // 注意看,这一层级的路由,没有设置 component
      path: 'clients',
      meta: {
        title: '客户管理'
      },
      children: [
        {
          path: 'list',
          component: () => import('@/views/list.vue'),
          meta: {
            title: '客户列表'
          }
        },
        {
          path: 'detail',
          component: () => import('@/views/detail.vue'),
          meta: {
            title: '客户详情'
          }
        }
      ]
    }
  ]
}

由于中间 clients 这层路由没有指定路由组件,顶级 Layout 组件里的 <RouterView> 将跳过它并直接显示 listdetail 子路由组件。

这样原有路由的数据结构无需做任何改动,而 <RouterView> 组件处理的其实就是一个二级的嵌套结构,所以缓存问题也迎刃而解了。

并且得益于这个特性,面包屑导航功能也能通过 $route.matched 实现。

扩展

为了避免用户误配置了中间层级路由的 component 字段,导致缓存失效,最好还是将路由数据整体清洗一遍,将中间层级的 component 字段删除。

// 删除路由中间层级对应的组件
function deleteMiddleRouteComponent(routes: RouteRecordRaw[], isRoot = true) {
  const res: RouteRecordRaw[] = []
  routes.forEach((route) => {
    if (!isRoot && route.children) {
      delete route.component
      route.children = deleteMiddleRouteComponent(route.children, false)
    }
    res.push(route)
  })
  return res
}