keep-alive 嵌套路由踩坑之旅

721 阅读2分钟

keep-alive 嵌套路由踩坑之旅

前言

在使用 keep-alive 缓存嵌套路由时,遇到了缓存不被销毁的问题,导致上线后用户重复打开页面时出现内存溢出,最终导致页面崩溃。

image.png

在 Vue 开发工具中,可以看到子组件被重复缓存。

我使用了 tag-view 退出视图,但由于将 BasicLayout 设置为常驻缓存,导致无法清理缓存。经过两天的排查,发现其实解决方案非常简单。

我的路由结构

router.js

const Layout = () => import('@/views/layout/Layout')
const BasicLayout = () => import('@/views/layout/BasicLayout')

{
  path: '/spray',
  redirect: '/spray/flavor',
  component: Layout,
  name: 'spray',
  meta: { title: i18n.t('router.index.2hzd72'), icon: 'canping' },
  children: [
    {
      path: 'flavor',
      name: 'flavor',
      meta: { title: i18n.t('router.index.5h54nw') },
      redirect: '/spray/flavor/demands',
      component: BasicLayout,
      children: [
        {
          path: 'demands',
          name: 'flavorDemands',
          meta: { title: i18n.t('router.index.hrj6g5') },
          component: () => import('@/views/spray/flavor/demands'),
          id: 0,
        },
      ]
    }
  ]
}

AppMain.vue

<template>
  <section class="app-main">
    <transition name="fade-transform" mode="out-in">
      <keep-alive
        :include="['BasicLayout', 'Layout', ...cachedViews]"
        :max="30"
      >
        <router-view :key="$route.fullPath" />
      </keep-alive>
    </transition>
  </section>
</template>

BasicLayout.vue

<template>
  <transition name="fade-transform" mode="out-in">
    <keep-alive :include="['BasicLayout', 'Layout', ...cachedViews]" :max="30">
      <router-view :key="$route.fullPath" />
    </keep-alive>
  </transition>
</template>

动态修改 BasicLayout 名称

BasicLayout.vue

const name =
  window.location.hash
    .split('/')
    .slice(2)
    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
    .join('') + 'Wrap'

export default {
  name: name,
  ...
}

这种方法不可行,因为 BasicLayout 组件是共享的,只会加载一次,动态组件名的方式无法实现。同时,在 tag-view 退出时需要删除缓存。

一些坑

  1. 嵌套问题<keep-alive> 不能嵌套,嵌套生效仅限于外层组件。
  2. 匹配机制:匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称(父组件 components 选项的键值)。匿名组件不能被匹配。
  3. 直系子组件:只对直系子组件生效。如果在其中有 v-for,则不会工作。
  4. 最大缓存max 设置相当于队列,最老的组件会被销毁,不再缓存。
  5. 组件名匹配includeexclude 匹配时使用组件名,如果没有设置 name,则不会缓存,和路由的名字无关。
  6. router-view 的 keyrouter-view 需要设置 key
  7. 子组件中的 router-view:子组件中的 router-view 不需要设置 key,如果设置了,父组件命中缓存时,子组件仍会被激活。
  8. 特殊处理:使用 BasicLayout 时,Layout 需要对子路由进行特殊处理。

修改后的代码

AppMain.vue

<template>
  <section class="app-main">
    <transition name="fade-transform" mode="out-in">
      <keep-alive :include="[...hasBasicLayout, 'Layout', ...cachedViews]" :max="30">
        <router-view :key="$route.fullPath" v-slot="{ Component }">
          <component :is="Component" />
        </router-view>
      </keep-alive>
    </transition>
  </section>
</template>

<script>
import { asyncRouterMap } from '@/router/index'

// 找到以组件名 name 作为“中间件”的所有路由
const findRouterWith = (name) => {
  const fn = (routes, newRoutes = []) => {
    routes.forEach((item) => {
      if (item.children && item.children.length > 0) {
        if (getFileNameByFunContext(item.component.toString()) === name) {
          newRoutes.push(item.children)
        }
        fn(item.children, newRoutes)
      }
    })
    return newRoutes.flat(Infinity)
  }
  return fn
}

// 通过函数内容获取文件名
const getFileNameByFunContext = (str) => {
  const [file = ''] = str.match(/".+"/)
  return file.replace(/(.*\/)*([^.]+).*/gi, '$2')
}

// 获取需进行缓存的页面
const getCachesByRoutes = (routes = []) => {
  const children = []
  const caches = routes
    .filter((o) => {
      // 有children说明进行了路由嵌套,需记录“中间件”
      if (o.children) {
        children.push(o.component)
      }
      // 过滤掉“中间件”和不需要缓存的组件
      return !o.children
    })
    .map((o) => o.name)

  if (children.length > 0) {
    // 路由嵌套的组件也需include
    children.forEach((fun) => {
      caches.push(getFileNameByFunContext(fun.toString()))
    })
  }
  return [...new Set(caches)] // 排重
}

export default {
  name: 'AppMain',
  data() {
    return {
      specialCache: [], // 特殊缓存逻辑
    }
  },
  computed: {
    cachedViews() {
      return this.$store.state.tagsView.cachedViews
    },
    hasBasicLayout() {
      for (let i = 0; i < this.cachedViews.length; i++) {
        if (this.specialCache.includes(this.cachedViews[i])) {
          return ['BasicLayout']
        }
      }
      return []
    },
  },
  created() {
    const target = findRouterWith('BasicLayout')(asyncRouterMap)
    this.specialCache = getCachesByRoutes(target)
  },
}
</script>

最终 getFileNameByFunContext 方法在UAT 不好用,名字被混淆了,所以 specialCache 直接写死了,虽然不太优雅,如果有更好方法,可以提醒下

BasicLayout.vue

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

在这里,router-view 不需要加 key

优化后效果

image-1.png

总结

我最初定位问题的方向是错误的,经过反复测试和调整,最终找到了合适的解决方案。

参考文献