一、前言
经典事故:列表页翻到第 5 页、筛了 3 个条件 → 点进详情 → 返回 → 全部重置回第 1 页。keep-alive(030 篇学过基础)就是解药,本篇讲企业级用法:按路由 meta 动态控制谁缓存,以及 onActivated 里"缓存后要不要刷新数据"的取舍。
二、keep-alive 缓存的本质
vue
<router-view v-slot="{ Component }">
<keep-alive :include="caches">
<component :is="Component" />
</keep-alive>
</router-view>
三个关键认知:
- 缓存的是组件实例:切走时组件不销毁(不触发 unmounted),切回来不重新创建------数据、滚动位置原样保留
- include 按组件名匹配 :值是组件的 name,不是路由 path!匹配不到就等于没缓存
- 配套生命周期 :被缓存的组件每次"回来"触发
onActivated、"离开"触发onDeactivated(代替 onMounted 的重复触发)
三、组件名怎么来:script setup 用 defineOptions
include 匹配组件名,但 <script setup> 默认没有显式 name(隐式用文件名)。为了 include 不踩坑,给页面组件显式声明 name(Vue 3.3+ 内置宏):
vue
<!-- src/views/GoodsListView.vue -->
<script setup lang="ts">
defineOptions({ name: 'GoodsList' }) // ★ 3.3+ 内置,无需插件
// ...其余代码
</script>
四、企业级方案:路由 meta + 动态 include
把"哪些页面要缓存"配置在路由 meta 里,Layout 里动态收集:
ts
// src/router/index.ts
{
path: '/',
component: () => import('@/layout/Layout.vue'),
children: [
{
path: 'goods/list',
component: () => import('@/views/GoodsListView.vue'),
meta: { title: '商品列表', keepAlive: true } // ★ 要缓存的页面
},
{
path: 'goods/detail/:id',
component: () => import('@/views/GoodsDetailView.vue'),
meta: { title: '商品详情' } // 不缓存
}
]
}
vue
<!-- src/layout/Layout.vue 的内容区替换为 -->
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// ★ 动态 include:只缓存 meta.keepAlive 的页面组件名
// 组件名与路由 name 约定一致,收集起来最省心
const caches = computed(() =>
router.getRoutes()
.filter((r) => r.meta?.keepAlive)
.map((r) => r.name as string)
)
</script>
<template>
<el-main>
<router-view v-slot="{ Component }">
<keep-alive :include="caches">
<component :is="Component" />
</keep-alive>
</router-view>
</el-main>
</template>
(路由对象加 name: 'GoodsList',与组件 defineOptions 的 name 一致。)
五、onActivated:缓存后要不要重新拉数据
缓存不是无脑"完全不动",典型策略:
vue
<!-- src/views/GoodsListView.vue -->
<script setup lang="ts">
import { ref, onMounted, onActivated } from 'vue'
const keyword = ref('')
const list = ref([])
onMounted(() => {
loadList() // 首次进入:正常加载
})
onActivated(() => {
// 从详情返回:默认啥都不做 → 状态原样保留(滚动位置、搜索、页码)
// ★ 按需刷新场景:详情页可能改了数据,返回时要更新列表
refreshIfDirty()
})
/** 详情页编辑后标记,回来重新拉数据 */
async function refreshIfDirty() {
const edited = sessionStorage.getItem('goods-edited')
if (edited) {
sessionStorage.removeItem('goods-edited')
await loadList() // ★ 只刷新数据,不重置筛选和页码
}
}
async function loadList() { /* 调接口,不影响 keyword/query */ }
</script>
决策表:
| 场景 | onActivated 做什么 |
|---|---|
| 纯浏览(详情只是看) | 什么都不做,完美还原现场 |
| 详情可能改数据 | 脏标记 → 只重拉数据,不动筛选/页码 |
| 需要每次都最新 | 不缓存该页(列表实时性 > 体验) |
六、踩坑记录
- include 写了路由 path :
include="['/goods/list']"永远匹配不上------include 匹配组件 name - script setup 组件没 name:隐式文件名在 Windows/大小写、路径变动时易出幺蛾子------页面组件一律 defineOptions 显式声明
- keep-alive 包错位置 :包在 router-view 外层但没接 v-slot 的写法不生效------必须
v-slot="{ Component }"+<component :is> - 缓存页面太多内存暴涨 :每份缓存都是完整组件实例(含 DOM)------只缓存列表类页面,详情/表单页不缓存;再加
:max="10"限上限(LRU 淘汰) - onMounted 与 onActivated 双触发:首次进入 mounted 和 activated 都会触发,数据拉两遍------数据请求只放 onMounted,activated 里只做"按需刷新"
- 缓存了但期望每次进来自动刷新:这是需求矛盾不是 bug------要么不缓存,要么脏标记按需刷
七、今日小结
- keep-alive 缓存组件实例:切走不销毁、回来不重建,现场完美还原
- include 匹配 组件 name(defineOptions 显式声明),不是路由 path
- 企业做法:路由 meta.keepAlive + Layout 动态收集 include
- onActivated 策略:纯浏览不动 / 脏标记刷数据不动筛选 / 实时页不缓存
下篇预告
一页 1 万条数据渲染直接卡死浏览器?DOM 数量才是元凶。下一篇 074 长列表优化:手写一个虚拟列表搞懂原理,再用 vue-virtual-scroller 落地。