vue3页面缓存-keepAlive
很多人第一次用 Vue3 做页面缓存,通常都是在外层包一层 <KeepAlive>,结果很快就会遇到几个问题:
- 不是所有页面都该缓存
- 返回列表页时,数据保住了,但滚动位置丢了
- 某些页面离开后其实应该清缓存,否则会看到旧状态
这次在项目里正好落了一版页面缓存,场景是 myOrder 列表页:从列表进入详情再返回时,列表状态和滚动位置都保留;但如果从 myOrder 返回首页,则主动清掉缓存,下次重新进入就是全新页面。
这篇文章就按这次实战改动,拆解一套更可控的 Vue3 KeepAlive 方案。
一、先说结论:不要只会"包一层 KeepAlive"
如果只是这样写:
vue
<router-view v-slot="{ Component }">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</router-view>
它只能解决"组件实例不销毁"这个问题,但还不够。
真正可用的页面缓存,至少要补上 3 个能力:
- 只缓存指定页面,而不是全站缓存
- 能在特定路由跳转时清除缓存
- 能恢复列表滚动位置,而不只是保住响应式数据
这次的实现就是围绕这 3 点展开的。
二、实现目标
先明确这次 myOrder 页面缓存的行为:
- 从
myOrder进入其他业务页,再返回myOrder,列表数据保留 - 搜索条件、分页状态、接口结果都保留
- 列表滚动位置保留
- 但如果从
myOrder回到home,则移除缓存,下次重新进入重新加载
这比"永远缓存"更符合真实业务。
三、第一步:在 App.vue 接入 KeepAlive
先在应用入口把 router-view 改成插槽模式,然后接上 <KeepAlive>。
vue
<router-view v-slot="{ Component }">
<KeepAlive
:max="3"
:include="keepAliveViewNames"
>
<component :is="Component" />
</KeepAlive>
</router-view>
这里有两个重点。
1. include 不写死,改成动态名单
如果直接写成:
vue
<KeepAlive include="myOrder" />
后面页面一多会很难维护,所以这里把缓存名单做成响应式数组 keepAliveViewNames,由路由统一控制。
2. max 一定要限制
KeepAlive 本质上是在缓存组件实例,不做上限控制,页面多了之后容易带来内存占用问题。这里我先给了 3,实际项目可以按业务量调整。
四、第二步:单独维护缓存页面名单
我新增了一个文件:src/router/keepAlive.ts
ts
import { ref } from 'vue'
const keepAliveInclude = ref<string[]>([])
export const keepAliveViewNames = keepAliveInclude
export const addKeepAliveView = (name: string) => {
if (!keepAliveInclude.value.includes(name)) {
keepAliveInclude.value.push(name)
}
}
export const removeKeepAliveView = (name: string) => {
keepAliveInclude.value = keepAliveInclude.value.filter(item => item !== name)
}
这层的作用很简单:把"哪些页面该缓存"的控制权,从组件里抽出来,统一交给路由。
这样做有两个好处:
- 页面组件本身不用关心自己何时加入缓存名单
- 后面要做"离开某页面时清缓存"也更方便
五、第三步:在路由守卫里动态加入 / 移除缓存
接下来是这次方案的核心。
在 src/router/index.ts 里加了两段辅助方法:
ts
const getKeepAliveName = (route: any) => {
return route.meta?.keepAliveName as string | undefined
}
const shouldDropKeepAlive = (from: any, to: any) => {
const routeNames = from.meta?.dropKeepAliveWhenLeaveTo as string[] | undefined
if (!routeNames?.length) {
return false
}
return routeNames.includes(String(to.name))
}
然后在 beforeEach 里处理:
ts
router.beforeEach((to: any, from: any, next: any) => {
const toKeepAliveName = getKeepAliveName(to)
const fromKeepAliveName = getKeepAliveName(from)
if (toKeepAliveName) {
addKeepAliveView(toKeepAliveName)
}
if (fromKeepAliveName && shouldDropKeepAlive(from, to)) {
removeKeepAliveView(fromKeepAliveName)
}
next()
})
这段逻辑的意思是:
- 进入某个声明了
keepAliveName的页面时,把它加入缓存名单 - 离开当前页时,如果目标路由命中了
dropKeepAliveWhenLeaveTo,就把当前页从缓存名单里移除
这样,缓存就不是"永久保留",而是"按跳转关系精确控制"。
六、第四步:把缓存策略写到路由 meta
页面是否缓存,不要在页面里写死,最好挂到路由配置上。
在 src/router/myOrder.ts 里这样配:
ts
export const myOrder = [
{
path: '/myOrder',
name: 'myOrder',
component: () => import('@/views/myOrder/index.vue'),
meta: {
title: 'My Order',
customBackTo: '/home',
leftButtonText: '',
hasRightButton: false,
keepAliveName: 'myOrder',//新增
dropKeepAliveWhenLeaveTo: ['home'],//新增
},
},
]
这里两个字段分别代表:
keepAliveName:进入这个路由时,把哪个组件名加入includedropKeepAliveWhenLeaveTo:当前页跳到哪些路由时,要清掉当前缓存
这个写法很适合中后台或 H5 业务页,因为很多时候"是否缓存"本来就是路由层面的策略,不是纯组件逻辑。
七、第五步:组件名必须和 include 对上
这是 KeepAlive 最容易踩的坑。
include 匹配的不是路由名,而是组件的 name。所以页面组件里必须显式声明:
vue
<script setup lang="ts">
defineOptions({
name: 'myOrder',
})
</script>
然后这个 name 要和路由 meta 里的 keepAliveName 一致:
ts
keepAliveName: 'myOrder'
如果这两个名字对不上,KeepAlive 看起来包上了,实际上不会生效。
八、第六步:缓存组件实例,不等于自动恢复滚动位置
这也是实战里最常见的误区。
很多人以为页面被 KeepAlive 缓存后,返回列表页滚动位置就一定还在。实际上不一定,尤其是滚动容器不是 window、而是页面内部某个容器时,通常还得自己存。
这次在 src/views/myOrder/index.vue 里补了这一段:
ts
const contentScrollTop = ref(0)
const getScrollContainer = () => {
return document.querySelector('.content') as HTMLElement | null
}
onActivated(() => {
nextTick(() => {
const container = getScrollContainer()
if (!container) {
return
}
container.scrollTop = contentScrollTop.value
})
})
onBeforeRouteLeave(() => {
const container = getScrollContainer()
if (!container) {
return
}
contentScrollTop.value = container.scrollTop
})
这里的思路是:
- 离开页面前,记录当前滚动容器的
scrollTop - 页面再次被激活时,用
onActivated把滚动位置恢复回去
为什么不用 onMounted?
因为被 KeepAlive 缓存的组件再次显示时,不会重新走 onMounted,而是走 onActivated。这点一定要分清。
九、这套方案为什么比"全局缓存"更稳
我更推荐这套方案,原因有 4 个。
1. 缓存范围可控
不是所有页面都缓存,只缓存真正需要"返回保留状态"的页面。
2. 支持定向失效
比如这次 myOrder -> home 时主动失效,就能避免用户从首页重新进入后看到旧列表状态。
3. 路由策略集中
缓存规则都挂在 route.meta 和路由守卫里,不会散落在各个页面组件中。
4. 更接近真实业务
很多业务不是"要不要缓存"这么简单,而是:
- 从 A 到 B 要缓存
- 从 A 到首页不要缓存
- 从筛选页返回要保留条件
- 从主入口重新进入要重新加载
这套写法正好能覆盖这类需求。
十、几个容易踩坑的点
1. include 匹配的是组件名,不是路由名
这一点最关键。你路由叫 myOrder 不代表就能缓存,组件必须有:
ts
defineOptions({ name: 'myOrder' })
2. 只缓存组件,不会自动处理你的副作用
比如:
- 定时器
- WebSocket
- 页面轮询
- 滚动恢复
这些都要自己在 onActivated / onDeactivated 或路由生命周期里处理。
3. 不要滥用缓存
以下页面通常不建议缓存:
- 数据强依赖实时性的页面
- 很重的大表格 / 地图 / 富媒体页面
- 离开后必须清空敏感数据的页面
4. max 不要省
缓存越多,实例越多。移动端 H5 更要注意上限。
十一、可以直接复用的落地步骤
如果你也要在项目里加页面缓存,可以按这个顺序来:
- 在
App.vue给router-view外层加<KeepAlive> - 新建一个
keepAlive.ts,统一维护include名单 - 在路由
beforeEach里处理缓存加入和移除 - 在目标路由
meta上声明keepAliveName - 在页面组件里显式声明
defineOptions({ name }) - 如果是列表页,再补
onBeforeRouteLeave + onActivated做滚动恢复
这套流程改动不大,但已经能覆盖大多数"列表页返回保留现场"的场景。
十二、最后总结
Vue3 的页面缓存,核心不是"会不会用 <KeepAlive>",而是你有没有把这几个问题想清楚:
- 哪些页面该缓存
- 什么时候该失效
- 页面返回时要恢复什么
我这次的落地方案,本质上就是:
- 用
KeepAlive保住组件实例 - 用路由
meta + beforeEach控制缓存名单 - 用
onActivated + onBeforeRouteLeave恢复滚动现场
如果你的项目里也有"列表进详情再返回"的需求,这套方案基本可以直接复用。
如果你后面还想继续扩展,也可以在这套基础上再加:
- 按 query / params 维度决定是否复用缓存
- 进入页面时按条件强制刷新数据
- 封装成通用的 keepAlive 路由管理方案