keep-alive使用
TIP
当 使用了 vue-router 时,无论路由实际对应的 vue 组件是否发生了实质性的界面更新,只要进行了路由切换,无论是否有用 keep-alive 缓存组件,都会触发路由对应 vue 组件的onUpdated钩子
作用
将组件缓存,并保持组件状态(即组件的数据状态)
常用于在哪些地方?
组件缓存和路由缓存。但 keep-alive 的缓存清除不怎么方便
上面这句话啥意思?
因为keep-alive无法直接控制缓存的释放。
网上很多解决缓存清除问题的方案,如以下文章中记录的方式。
解读 keep-alive:Vue3 中手动清理 keep-alive 组件缓存的一个解决方案
再续:解读 keep-alive:Vue3 中手动清理 keep-alive 组件缓存的一个解决方案
注意事项:
keep-alive默认情况下是缓存所有组件的,那如果不希望缓存所有组件,就需要使用include和exclude指定哪些组件要缓存,哪些组件不要缓存。且exclude的优先级要高于include。如果指定了include或exclude, keep-alive内部是通过组件name,组件name,组件name重要的事情说 3 遍,判断组件是否需要缓存。而非路由name,而非路由name,而非路由name
从路由信息中获取组件 name
- 手工通过
mate属性指定,可行但不推荐 - 通过
$router.currentRoute.value.matched遍历这个数组,再从数组每个元素的item.components?.default?.name或item.components?.default.__name获取。 - 如果路由对应组件是通过
component: () => import('@/pages/Home.vue')这种方式定义的,那么 2 步骤的返回值会是一个返回 Promise 值的函数,因此需要先执行该函数,拿到 promise 中的内容之后,再通过 2 步骤的方式获取组件名
TIP
第二种方式为什么是两个属性?到底应该从哪个属性拿到?
当 SFC 的组件定义中有使用 name 属性指定组件名时, routeInfo.components?.default?.name 可以取到值, 当 SFC 的组件定义中没使用 name 属性指定组件名时, routeInfo.components?.default?.name 取不到值,vue 3.2.34 或以上的版本会自动根据文件名生成对应的 name,但该 name 在__name属性上
特点
- 默认情况下
<KeepAlive>会缓存内部的所有组件实例,最大缓存数量为 10 个 - 可以通过
include和excludeprop来控制哪些组件需要缓存哪些无需缓存。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组 - 可以通过传入
maxprop来限制可被缓存的最大组件实例数。<KeepAlive>的行为在指定了max后类似一个LRU缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。
它会根据组件的 name 选项进行匹配,所以组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。
html
<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
<component :is="view" />
</KeepAlive>
<!-- 正则表达式 -->
<KeepAlive :include="/a|b/">
<component :is="view" />
</KeepAlive>
<!-- 数组 -->
<KeepAlive :include="['a', 'b']">
<component :is="view" />
</KeepAlive>
html
<KeepAlive :max="10">
<component :is="activeComponent" />
</KeepAlive>
TIP
在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成对应的 name 选项,无需再手动声明。
缓存实例的生命周期
当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。
一个持续存在的组件可以通过 onActivated() 和 onDeactivated() 注册相应的两个状态的生命周期钩子:
vue
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>
请注意:
onActivated在组件挂载时也会调用,并且onDeactivated在组件卸载时也会调用。- 这两个钩子不仅适用于
<KeepAlive>缓存的根组件,也适用于缓存树中的后代组件。
实例
keep-alive和vue-router结合,实现路由缓存,并结合emitter手动清除缓存
网上有很多实现该功能的类似的文章,但大部分是基于 vue2 的。有些方案甚至需要使用destory()方法,强制组件销毁,这是我极为不推荐的。最好采用下面的这种方式。
关于滚动条位置问题参考这篇文章:这大概是最全乎的 keep-alive 踩坑指南
vue
<!--
@file: KeepAliveWrapper.vue
@author: pan
-->
<script lang="ts">
export default {
name: 'KeepAliveWrapper',
}
</script>
<script setup lang="ts">
import { onUpdated, VNode } from 'vue'
import { RouteLocationNormalizedLoaded } from 'vue-router'
import { useKeepAliveWrapperIncludeData } from './useKeepAliveHelper'
interface KeepAliveWrapperProps {
/**
* keepAlive缓存组件数量
*/
keepAliveMax?: number
/**
* 是否启用keep-alive. 默认: true
*/
enableKeepAlive?: boolean
}
withDefaults(defineProps<KeepAliveWrapperProps>(), {
keepAliveMax: 2,
enableKeepAlive: true,
})
const { include, showRouterView } = useKeepAliveWrapperIncludeData()
onUpdated(() => {
console.log('KeepAliveWrapper onUpdated')
})
function buildCompKey(route: RouteLocationNormalizedLoaded, comp?: VNode) {
if (comp && comp.key) {
// 组件本身有key,则采用组件的key
return comp.key
}
if (route.meta.useFullPathAsRouteKey) {
// 路由配置中设置了使用fullpath作为key,则优先采用fullpath作为key
return route.fullPath
}
if (route.meta.routeKey) {
// 如果设置了routeKey则采用routeKey作为key
return route.meta.routeKey
}
// important: 当以上都没有的时候,应该返回undefined,让vue自行处理
return undefined
}
</script>
<template>
<!--
当 router-view v-slot="{ Component, route }" 通过这种形式使用,
且有通过 v-if 控制 component 的显示隐藏时,
必须设置 key,即使最终返回的是 undefined
-->
<router-view v-slot="{ Component, route }">
<keep-alive v-if="enableKeepAlive" :include="include" :max="keepAliveMax">
<component :is="Component" v-if="showRouterView" :key="buildCompKey(route, Component)"></component>
</keep-alive>
<component :is="Component" v-else-if="showRouterView" :key="buildCompKey(route, Component)"></component>
</router-view>
</template>
<style lang="scss" scoped></style>
ts
import emitter from '@/utils/emitter'
import { nextTick, ref } from 'vue'
import {
onBeforeRouteUpdate,
RouteComponent,
RouteLocationNamedRaw,
RouteLocationNormalizedLoaded,
RouteLocationRaw,
useRouter,
} from 'vue-router'
const addRouteCompKeepAliveEvt = Symbol('添加路由对应的vue组件的keep-alive缓存事件')
const removeRouteCompKeepAliveEvt = Symbol('删除路由对应vue组件的keep-alive缓存事件')
const refreshCurRouteEvt = Symbol('刷新当前路由事件')
export interface RouteNameCompName {
/**
* 路由的name属性值
*/
routeName: string
/**
* 组件的name属性值
*/
compName: string
/**
* 默认是否需要将 compName 对应的组件, 使用 keepAlive 进行缓存
*/
keepAlive: boolean
}
/**
* key为路由的name属性值
*/
const routeNameCompNameMap = new Map<string, RouteNameCompName>()
/**
* 从路由组件中获取真实的组件名
*
* 暂时发现 RouteComponent 有两种情况:
* 一. RouteComponent 就是一个 vue 的 Component, 此时又分为两种情况:
* 1. vue组件未定义组件name:那么vue会自动根据文件名生成组件名,并放入 `__name` 属性
* 2. vue组件定义了组件name: 那么此时要取 name 值,则直接访问 name 属性即可
* 二. RouteComponent 是一个函数。此时的路由引入方式为: `component: () => import('@/pages/Home.vue')`
* 要获取到这个函数表示组件的组件名,需要先执行这个函数,执行这个函数会返回一个Promise值,Promise的内容
* 就是组件对象,这个组件对象的name获取也像上面一的情况一样,需要分为两种情况获取
*
* @param {RouteComponent<string>} routeComp 路由组件
*
* @return {Promise<string>} 组件名
*/
async function getCompName(routeComp: RouteComponent): Promise<string> {
let tmp = routeComp
if (tmp instanceof Function) {
// @ts-ignore
tmp = await routeComp()
// @ts-ignore
return tmp.default.name || tmp.default.__name
} else {
// @ts-ignore
return tmp.name || tmp.__name
}
}
/**
* 从路由信息中获取路由名,组件名以及组件是否需要缓存
*
* @param route 路由信息
*/
async function findRouteNameCompName(route: RouteLocationNormalizedLoaded): Promise<RouteNameCompName | undefined> {
let ret: RouteNameCompName | undefined = undefined
const currentRoute = route
const curRouteName = currentRoute.name
if (!curRouteName) {
return ret
}
ret = routeNameCompNameMap.get(curRouteName as string)
if (ret) {
return ret
}
const { keepAlive = true } = currentRoute.meta
const matchedRouteArr = currentRoute.matched
for (const routeInfo of matchedRouteArr) {
if (routeInfo.name === curRouteName) {
/*
important: 当SFC的组件定义中有使用name属性指定组件名时, routeInfo.components?.default?.name 可以取到值
important: 当SFC的组件定义中没使用name属性指定组件名时, routeInfo.components?.default?.name 取不到值,vue 3.2.34 或以上的版本会自动根据文件名生成对应的name,但该name在__name上
*/
if (!routeInfo.components || !routeInfo.components.default) {
return ret
}
const compName = await getCompName(routeInfo.components.default)
return {
compName,
routeName: curRouteName as string,
keepAlive: keepAlive as boolean,
}
}
}
}
const CLEAR_KEEP_ALIVE_PARAM_NAME = '__clearKeepAlive'
/**
* 如果route的query参数包含 __clearKeepAlive 的参数且为true,则获取到该路由对应的组件名
*
* @return {[type]} [return description]
*/
async function getRouteNameCompNameIfNeedClearKeepAlive(route: RouteLocationNormalizedLoaded) {
if (route.query[CLEAR_KEEP_ALIVE_PARAM_NAME] === 'true' && route.name) {
return await findRouteNameCompName(route)
}
}
/**
* 智能添加 keep-alive 缓存
*
* @param {RouteNameCompName} _data 需要智能添加缓存的路由名以及组件名
*
*/
export function addCompKeepAliveByEvt(_data: RouteNameCompName) {
emitter.emit(addRouteCompKeepAliveEvt, _data)
}
/**
* 刷新当前路由
*/
export function refreshCurRouteByEvt() {
emitter.emit(refreshCurRouteEvt)
}
/**
* 智能添加 keep-alive 缓存(通过事件方式实现)
* @param {RouteLocationNormalizedLoaded} route 需要智能添加缓存的路由信息
*/
export async function aiAddRouteKeepAliveByEvt(route: RouteLocationNormalizedLoaded) {
const _data = await findRouteNameCompName(route)
if (_data) {
// console.log('aiAddCacheFromRouteInfo', _data)
emitter.emit(addRouteCompKeepAliveEvt, _data)
}
}
/**
* 智能删除keep-alive缓存
*
* @param {RouteLocationNormalizedLoaded} route 需要智能删除keep-alive缓存的路由信息
*
* @return {[type]} [return description]
*/
export async function aiRemoveRouteKeepAliveByEvt(route: RouteLocationNormalizedLoaded) {
const compNameArr: string[] = []
const routeNameCompName = await getRouteNameCompNameIfNeedClearKeepAlive(route)
if (routeNameCompName) {
compNameArr.push(routeNameCompName.compName)
}
if (compNameArr.length > 0) {
return new Promise<void>(resolve => {
const params: RemoveRouteCompKeepAliveEvtParam = {
compNameArr,
resolve,
}
emitter.emit(removeRouteCompKeepAliveEvt, params)
})
}
return Promise.resolve()
}
/**
* 智能判断是让keep-alive组件缓存路由对应的vue组件还是清除路由对应的vue组件,并通过事件方式实现
*/
export async function aiAddOrRemoveRouteKeepAliveByEvt(route: RouteLocationNormalizedLoaded) {
await aiAddRouteKeepAliveByEvt(route)
await aiRemoveRouteKeepAliveByEvt(route)
}
/**
* 删除组件的keep-alive缓存
*
* @param {string[]} compNameArr 需要清除keep-alive缓存的组件名数组
*
*/
export function removeCompArrKeepAliveByEvt(compNameArr: string[]) {
if (compNameArr.length > 0) {
return new Promise<void>(resolve => {
const params: RemoveRouteCompKeepAliveEvtParam = {
compNameArr,
resolve,
}
emitter.emit(removeRouteCompKeepAliveEvt, params)
})
}
return Promise.resolve()
}
interface RemoveRouteCompKeepAliveEvtParam {
compNameArr: string[]
resolve: () => void
}
/**
* 删除路由的keep-alive缓存
*
* @param {string[]} routeNameArr 需要清除keep-alive缓存的路由名数组
*
*/
export function removeRouteArrKeepAliveByEvt(routeNameArr: string[]) {
if (routeNameArr.length > 0) {
const compNameArr: string[] = []
routeNameArr.forEach(routeName => {
const compNameAndRouteName = routeNameCompNameMap.get(routeName)
if (compNameAndRouteName) {
compNameArr.push(compNameAndRouteName.compName)
} else {
console.warn(`当前缓存的路由名中,未找到[${routeName}]路由对应的组件名`)
}
})
return new Promise<void>(resolve => {
const params: RemoveRouteCompKeepAliveEvtParam = {
compNameArr,
resolve,
}
emitter.emit(removeRouteCompKeepAliveEvt, params)
})
}
return Promise.resolve()
}
/**
* 删除路由的keep-alive缓存
*
* @param {string} routeName 需要清除keep-alive缓存的路由名
*/
export function removeRouteKeepAliveByEvt(routeName: string) {
return removeRouteArrKeepAliveByEvt([routeName])
}
/**
* 删除组件的keep-alive缓存
*
* @param {string} compName 需要清除keep-alive缓存的组件名
*
*/
export function removeCompKeepAlive(compName: string) {
return new Promise<void>(resolve => {
const params: RemoveRouteCompKeepAliveEvtParam = {
compNameArr: [compName],
resolve,
}
emitter.emit(removeRouteCompKeepAliveEvt, params)
})
}
export function useKeepAliveRoute() {
const $router = useRouter()
/**
* 通过push方式转入路由,且禁止路由使用keep-alive的缓存(调用该方法必须设置name值)
*
* 内部实际调用的 removeOneRouteKeepAliveAndToOtherRoute 方法
*
* @param {RouteLocationRaw} routeLocation 需要转入的路由
*/
function pushAndDisableKeepAlive(routeLocation: RouteLocationNamedRaw) {
if (!routeLocation.name) {
throw new Error('调用 pushAndDisableKeepAlive 方法, routeLocation 参数必须指定 name 值')
}
return removeOneRouteKeepAliveAndToOtherRoute(routeLocation.name as string, routeLocation)
}
/**
* 清除指定路由的keep-alive缓存,然后转入一个新路由
*
* @param clearKeepAliveRouteName 需要清除keep-alive缓存的路由名
* @param to 需要通过push方式跳转的路由
*/
function removeOneRouteKeepAliveAndToOtherRoute(clearKeepAliveRouteName: string, to: RouteLocationRaw) {
return removeRouteArrKeepAliveByEvt([clearKeepAliveRouteName]).then(() => {
return $router.push(to)
})
}
return { pushAndDisableKeepAlive, removeOneRouteKeepAliveAndToOtherRoute }
}
/**
* 获取KeepAliveWrapper组件的include数据
*/
export function useKeepAliveWrapperIncludeData() {
const include = ref<string[]>([])
const curRouteName = ref<string>()
/**
* 根据 routeNameCompName 更新 include 值
*
* @return {[type]} [return description]
*/
function updateIncludeByRouteNameCompName(routeNameCompName: RouteNameCompName) {
// 缓存路由名和组件名的映射关系
routeNameCompNameMap.set(routeNameCompName.routeName, routeNameCompName)
const idx = include.value.findIndex(item => item === routeNameCompName.compName)
if (routeNameCompName.keepAlive) {
// 需要添加缓存
if (idx < 0) {
console.log('include.value.push')
include.value.push(routeNameCompName.compName)
}
} else {
// 无需添加缓存
if (idx >= 0) {
console.log('include.value.splice')
include.value.splice(idx, 1)
}
}
}
/**
* 智能添加 keep-alive 缓存
* @param {RouteLocationNormalizedLoaded} route 需要智能添加缓存的路由信息
*/
async function aiAddRouteKeepAlive(route: RouteLocationNormalizedLoaded) {
const _data = await findRouteNameCompName(route)
if (_data) {
updateIncludeByRouteNameCompName(_data)
}
}
/**
* 智能删除keep-alive缓存. 如果 route 的 query 参数包含 __clearKeepAlive 参数且值为 true,则会清除 route 的 keep-alive 缓存
*
* @param {RouteLocationNormalizedLoaded} route 需要智能删除keep-alive缓存的路由信息
*
* @return {[type]} [return description]
*/
async function aiRemoveRouteKeepAlive(route: RouteLocationNormalizedLoaded) {
const compNameArr: string[] = []
const routeNameCompName = await getRouteNameCompNameIfNeedClearKeepAlive(route)
if (routeNameCompName) {
compNameArr.push(routeNameCompName.compName)
}
updateIncludeByCompNameArr(compNameArr)
if (routeNameCompName) {
// 等待vue将上面的微任务执行完毕(上面的微任务包含一些什么?1.index属性值的更新 2.界面的重新渲染)
await nextTick()
// 执行到这里vue已经将界面重新渲染完毕(keep-alive中的缓存已经更新)。再根据组件的初始keepAlive状态,决定是否需要再将当前的组件名放入keep-alive的include数组中
updateIncludeByRouteNameCompName(routeNameCompName)
}
}
/**
* 智能判断是让keep-alive组件缓存路由对应的vue组件还是清除路由对应的vue组件
*/
async function aiAddOrRemoveRouteKeepAlive(route: RouteLocationNormalizedLoaded) {
await aiAddRouteKeepAlive(route)
await aiRemoveRouteKeepAlive(route)
}
/**
* 通过 compNameArr 删除 include 中的值
*
* @param {string[]} compNameArr 待删除的组件名
*/
function updateIncludeByCompNameArr(compNameArr: string[]) {
if (compNameArr && compNameArr.length > 0) {
compNameArr.forEach(compName => {
// 找到然后删除
const idx = include.value.findIndex(item => item === compName)
if (idx >= 0) {
include.value.splice(idx, 1)
}
})
}
}
onBeforeRouteUpdate(async to => {
if (to.name) {
curRouteName.value = to.name as string
}
await aiAddOrRemoveRouteKeepAlive(to)
})
/**
* 监听添加keep-alive缓存的事件
*
* @param {unknown} _data 需要添加keep-alive缓存的组件名和路由名
*/
emitter.on(addRouteCompKeepAliveEvt, (_data: unknown) => {
const routeNameCompName = _data as RouteNameCompName
updateIncludeByRouteNameCompName(routeNameCompName)
// console.log('addRouteCompCacheEvt', addRouteCompKeepAliveEvt)
})
/**
* 监听组件 keep-alive 缓存删除事件
*
* @param {unknown} _data 需要删除 keep-alive 缓存的组件名数组
*
* @return {[type]} [return description]
*/
emitter.on(removeRouteCompKeepAliveEvt, (_data: unknown) => {
const { compNameArr, resolve } = _data as RemoveRouteCompKeepAliveEvtParam
updateIncludeByCompNameArr(compNameArr)
resolve()
})
const showRouterView = ref<boolean>(true)
emitter.on(refreshCurRouteEvt, () => {
// console.log(refreshCurRouteEvt, curRouteName.value, routeNameCompNameMap)
// 这里实际是让组件销毁
showRouterView.value = false
if (curRouteName.value) {
// 通过路由名找到组件名
const compNameAndRouteName = routeNameCompNameMap.get(curRouteName.value)
if (compNameAndRouteName) {
// 找到include中的组件名
const idx = include.value.findIndex(compName => compName === compNameAndRouteName.compName)
if (idx >= 0) {
// 删除这个组件名
include.value.splice(idx, 1)
}
}
}
nextTick(() => {
showRouterView.value = true
// console.log(refreshCurRouteEvt)
const compNameAndRouteName = curRouteName.value && routeNameCompNameMap.get(curRouteName.value)
if (compNameAndRouteName && compNameAndRouteName.keepAlive) {
// 如果原本这个组件就是支持缓存的,则继续让它支持缓存
include.value.push(compNameAndRouteName.compName)
}
})
})
return { include, showRouterView }
}
vue
<!--
@file: KeepAliveTest.vue
@author: pan
-->
<script lang="ts">
export default {
name: 'KeepAliveTest',
}
</script>
<script setup lang="ts">
import { onMounted, onUpdated } from 'vue'
import { useRouter } from 'vue-router'
import KeepAliveWrapper from './KeepAliveWrapper.vue'
import { refreshCurRouteByEvt, removeRouteKeepAliveByEvt, useKeepAliveRoute } from './useKeepAliveHelper'
const $router = useRouter()
function onChangeCompRoute(compName: string) {
$router.push({ name: compName })
}
onMounted(() => {
console.log('onMounted KeepAliveTest')
})
onUpdated(() => {
console.log('onUpdated KeepAliveTest')
})
const { removeOneRouteKeepAliveAndToOtherRoute } = useKeepAliveRoute()
/**
* 清除指定路由的keep-alive缓存,并转入一个新路由
*
* @param clearKeepAliveRouteName 需要清除keep-alive缓存的路由名
* @param toRouteName 需要通过push方式跳转的路由
*/
function onChangeCompRouteAndClearKeepAlive(clearKeepAliveRouteName: string, toRouteName: string) {
removeOneRouteKeepAliveAndToOtherRoute(clearKeepAliveRouteName, {
name: toRouteName,
})
}
function onClearRouteKeepAlive(routeName: string) {
return removeRouteKeepAliveByEvt(routeName)
}
function onRefresh() {
refreshCurRouteByEvt()
}
const { pushAndDisableKeepAlive } = useKeepAliveRoute()
function onNoCacheToComp(routeName: string) {
pushAndDisableKeepAlive({ name: routeName })
}
function onNoCacheToCompByQueryParam(routeName: string) {
$router.push({ name: routeName, query: { __clearKeepAlive: 'true' } })
}
</script>
<template>
<div>
<div>KeepAliveTest</div>
<h3>使用keep-alive和router-view</h3>
<div>
<el-button @click="onChangeCompRoute('KeepAliveTest')"> 回到首页 </el-button>
<el-button @click="onChangeCompRoute('CompA')">到组件A路由</el-button>
<el-button @click="onChangeCompRoute('CompC')">到组件C路由</el-button>
<el-button @click="onChangeCompRoute('CompB')">到组件B路由</el-button>
<el-button @click="onChangeCompRoute('CompD')">到组件D路由</el-button>
</div>
<div>
<el-button @click="onChangeCompRouteAndClearKeepAlive('CompA', 'CompB')">
清除A组件路由keep-alive缓存并转入组件B路由
</el-button>
<el-button @click="onClearRouteKeepAlive('CompA')"> 清除A组件路由keep-alive缓存 </el-button>
<el-button @click="onRefresh">刷新当前路由</el-button>
<el-button @click="onNoCacheToComp('CompA')">
方式一:通过调用 pushAndDisableKeepAlive 方法,以无缓存的方式进入组件A
</el-button>
<el-button @click="onNoCacheToCompByQueryParam('CompA')">
方式二:通过指定query:__clearKeepAlive=true参数,以无缓存的方式进入组件A
</el-button>
</div>
<KeepAliveWrapper :keep-alive-max="4"></KeepAliveWrapper>
</div>
</template>
<style lang="scss" scoped></style>
不同路由指向同一个组件,keep-alive 能分别缓存页面状态?
答案是 keep-alive 实现不了这种功能。
因为 keep-alive 是根据组件名来判断是否需要缓存或清除缓存以及切换组件实例的激活和冻结状态的。 多个组件实例都是相同的 name,keep-alive 就不知道谁是谁了。
折中的解决办法
不要将多个路由指向同一个组件,而应该新建多个页面组件,每个组件只是作为 真实组件 的容器,然后多个路由页面分别指向不同的页面组件 然后在 keep-alive 的 include 属性中加入所有路由组件的 name,就可以实现分别缓存同一组件不同实例的状态了。
扩展阅读
Vue 中 keep-alive 对体验提升的作用与问题及解决方案
Vue.js - 路由 vue-router 的使用详解 6(keep-alive 使用 2:结合 router-view)
参考资料
Vue 实现刷新当前单个页面 适用于 keep-alive【vue3 适用】
vue-router 的 keep-alive(前进刷新,后退不刷新)