【Vue keep-alive】+【中后台路由缓存】:从include/exclude控制到路由meta标记,彻底搞懂页面缓存可控方案,避开组件无name、层级错误等高频坑!

📑 文章目录
- [一、先搞清楚:为什么要用 keep-alive?](#一、先搞清楚:为什么要用 keep-alive?)
- [二、基础用法:最简单的 keep-alive](#二、基础用法:最简单的 keep-alive)
- [2.1 在路由出口处包一层](#2.1 在路由出口处包一层)
- [三、include / exclude:精确控制「谁缓存、谁不缓存」](#三、include / exclude:精确控制「谁缓存、谁不缓存」)
- [3.1 参数说明](#3.1 参数说明)
- [3.2 用 include 做白名单](#3.2 用 include 做白名单)
- [3.3 用 exclude 做黑名单](#3.3 用 exclude 做黑名单)
- [3.4 关键点:组件必须有 name](#3.4 关键点:组件必须有 name)
- [3.5 常见坑:name 要和 include/exclude 里的完全一致](#3.5 常见坑:name 要和 include/exclude 里的完全一致)
- [四、路由 meta 标记:可维护性更好的方式](#四、路由 meta 标记:可维护性更好的方式)
- [4.1 路由配置](#4.1 路由配置)
- [4.2 根据 meta 动态计算 include](#4.2 根据 meta 动态计算 include)
- [4.3 Vue 2 中的写法](#4.3 Vue 2 中的写法)
- [五、更完整的实战结构:多级路由 + keep-alive](#五、更完整的实战结构:多级路由 + keep-alive)
- [5.1 目录结构示例](#5.1 目录结构示例)
- [5.2 路由配置](#5.2 路由配置)
- [5.3 MainLayout.vue 中的 keep-alive](#5.3 MainLayout.vue 中的 keep-alive)
- [5.4 子组件必须定义 name](#5.4 子组件必须定义 name)
- [六、生命周期:activated 与 deactivated](#六、生命周期:activated 与 deactivated)
- [6.1 典型用法:刷新列表、恢复滚动](#6.1 典型用法:刷新列表、恢复滚动)
- 七、常见问题与避坑
- 八、推荐规范小结
- 九、小结
- [🔍 系列模块导航](#🔍 系列模块导航)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一、先搞清楚:为什么要用 keep-alive?
日常写 Vue 项目时,从列表进详情再返回,页面会重新渲染,数据和滚动位置都会丢失,用户体验很差。
keep-alive 的作用就是:把匹配到的组件缓存在内存里,切换路由时不会销毁,而是保持上一次的状态。再返回时,数据、滚动条、表单输入都会保留。
- 适合做缓存的:列表页、搜索结果、表单(多步骤)、数据统计/图表等需要保留状态的页面。
- 不适合缓存的:登录页、404、纯静态说明页等。
[⬆ 返回目录](#⬆ 返回目录)
二、基础用法:最简单的 keep-alive
2.1 在路由出口处包一层
html
<!-- App.vue 或根布局组件 -->
<template>
<div id="app">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</template>
这样写会缓存所有路由组件,简单但不可控,一般不推荐。
[⬆ 返回目录](#⬆ 返回目录)
三、include / exclude:精确控制「谁缓存、谁不缓存」
3.1 参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
include |
String | RegExp |
exclude |
String | RegExp |
max |
Number | 最多缓存多少个组件,超出按 LRU 策略淘汰 |
这里的「名称」指的是组件的 name 选项 ,不是路由的 path 或 meta。
[⬆ 返回目录](#⬆ 返回目录)
3.2 用 include 做白名单
html
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="['ProductList', 'OrderList', 'Dashboard']">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
只有 name 为 ProductList、OrderList、Dashboard 的组件会被缓存,其余不缓存。
[⬆ 返回目录](#⬆ 返回目录)
3.3 用 exclude 做黑名单
html
<template>
<router-view v-slot="{ Component }">
<keep-alive exclude="LoginPage,NotFound">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
name 为 LoginPage、NotFound 的组件不会被缓存。
[⬆ 返回目录](#⬆ 返回目录)
3.4 关键点:组件必须有 name
html
<script setup>
defineOptions({
name: 'ProductList' // 必须!否则 include/exclude 无法匹配
})
</script>
<template>
<div>商品列表页</div>
</template>
如果组件没有 name,include / exclude 对它是无效的。
[⬆ 返回目录](#⬆ 返回目录)
3.5 常见坑:name 要和 include/exclude 里的完全一致
js
// ❌ 错误:路由配置里的 component 没有 name
{
path: '/product/list',
component: () => import('@/views/ProductList.vue')
}
// ProductList.vue 里必须写 defineOptions({ name: 'ProductList' })
// ✅ 正确:组件 name 和 include 数组里的字符串完全一致
// keep-alive :include="['ProductList']"
// ProductList.vue: defineOptions({ name: 'ProductList' })
[⬆ 返回目录](#⬆ 返回目录)
四、路由 meta 标记:可维护性更好的方式
通过路由的 meta 标记「是否需要缓存」,比在 keep-alive 里硬编码 include 数组更清晰,也更易维护。
4.1 路由配置
js
// router/index.js
const routes = [
{
path: '/product/list',
name: 'ProductList',
component: () => import('@/views/ProductList.vue'),
meta: { keepAlive: true } // 标记为需要缓存
},
{
path: '/product/detail/:id',
name: 'ProductDetail',
component: () => import('@/views/ProductDetail.vue'),
meta: { keepAlive: false } // 详情页一般不缓存
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { keepAlive: false }
}
]
[⬆ 返回目录](#⬆ 返回目录)
4.2 根据 meta 动态计算 include
html
<!-- App.vue -->
<template>
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.path" />
</keep-alive>
</router-view>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// 从路由配置中筛选出 meta.keepAlive === true 的组件名
const cachedViews = computed(() => {
return router.getRoutes()
.filter(route => route.meta?.keepAlive && route.name)
.map(route => route.name)
})
</script>
这样,只要在路由里改 meta.keepAlive,就能控制哪些页面被缓存,不用再去改 keep-alive 的配置。
[⬆ 返回目录](#⬆ 返回目录)
4.3 Vue 2 中的写法
js
computed: {
cachedViews() {
return this.$router.options.routes
.filter(route => route.meta?.keepAlive && route.name)
.map(route => route.name)
}
}
[⬆ 返回目录](#⬆ 返回目录)
五、更完整的实战结构:多级路由 + keep-alive
很多项目会区分「有布局的页面」和「无布局的页面」,布局里再包一层 router-view,需要把 keep-alive 放在正确的层级。
5.1 目录结构示例
views/
├── layout/
│ └── MainLayout.vue # 主布局,包含侧边栏 + 内容区
├── product/
│ ├── ProductList.vue # 商品列表(缓存)
│ └── ProductDetail.vue # 商品详情(不缓存)
└── Login.vue # 登录(不缓存)
[⬆ 返回目录](#⬆ 返回目录)
5.2 路由配置
js
const routes = [
{
path: '/login',
component: () => import('@/views/Login.vue'),
meta: { keepAlive: false }
},
{
path: '/',
component: () => import('@/views/layout/MainLayout.vue'),
children: [
{
path: 'product/list',
name: 'ProductList',
component: () => import('@/views/product/ProductList.vue'),
meta: { keepAlive: true, title: '商品列表' }
},
{
path: 'product/detail/:id',
name: 'ProductDetail',
component: () => import('@/views/product/ProductDetail.vue'),
meta: { keepAlive: false, title: '商品详情' }
}
]
}
]
[⬆ 返回目录](#⬆ 返回目录)
5.3 MainLayout.vue 中的 keep-alive
html
<!-- views/layout/MainLayout.vue -->
<template>
<div class="main-layout">
<aside class="sidebar">...</aside>
<main class="content">
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.path" />
</keep-alive>
</router-view>
</main>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const cachedViews = computed(() => {
// 只处理当前 layout 下的 children,避免把 Login 等算进来
const layout = router.getRoutes().find(r => r.path === '/')
if (!layout?.children) return []
return layout.children
.filter(route => route.meta?.keepAlive && route.name)
.map(route => route.name)
})
</script>
[⬆ 返回目录](#⬆ 返回目录)
5.4 子组件必须定义 name
html
<!-- ProductList.vue -->
<script setup>
defineOptions({ name: 'ProductList' })
</script>
[⬆ 返回目录](#⬆ 返回目录)
六、生命周期:activated 与 deactivated
被 keep-alive 缓存的组件,不会反复触发 mounted / unmounted,而是使用:
- activated:组件被激活(从别的路由切回来时)
- deactivated:组件被停用(切走时)
6.1 典型用法:刷新列表、恢复滚动
html
<script setup>
import { ref, onActivated } from 'vue'
import { useRoute } from 'vue-router'
const list = ref([])
const scrollTop = ref(0)
const route = useRoute()
// 首次进入或从详情返回,都可以在这里刷新
onActivated(() => {
loadList()
nextTick(() => {
listRef.value?.scrollTo(0, scrollTop.value)
})
})
// 离开时保存滚动位置
onDeactivated(() => {
scrollTop.value = listRef.value?.scrollTop ?? 0
})
async function loadList() {
const res = await fetchList(route.query)
list.value = res.data
}
</script>
[⬆ 返回目录](#⬆ 返回目录)
七、常见问题与避坑
7.1 缓存了不该缓存的页面
现象 :登录后退出,再进登录页,还能看到上次的账号。
原因 :登录页被 keep-alive 缓存了。
处理 :在 exclude 或 meta.keepAlive 里排除登录页,并确保组件有正确的 name。
7.2 以为缓存了,实际没缓存
现象:列表页返回后数据丢失。
排查:
- 组件是否定义了
name; name是否和include/cachedViews中完全一致;keep-alive是否包在了正确的router-view外层(多级路由时)。
7.3 多 Tab 页只缓存一个
现象 :有多个 Tab,切换后只保留最后一个的状态。
原因 :多个 Tab 可能共用同一个路由 path,没有用 key 区分实例。
处理 :给 component 加不同的 key,例如:
html
<component :is="Component" :key="route.fullPath" />
这样同一路由的不同参数会当成不同组件实例进行缓存。
7.4 缓存的页面需要「按需刷新」
可以在 meta 里加标识,或在 activated 里根据条件决定是否重新请求:
js
// 方式1:meta 里标记
meta: { keepAlive: true, refreshOnActivate: true }
// 方式2:activated 里判断
onActivated(() => {
if (route.query.refresh === '1') {
loadList()
}
})
[⬆ 返回目录](#⬆ 返回目录)
八、推荐规范小结
| 场景 | 建议做法 |
|---|---|
| 缓存策略 | 用路由 meta.keepAlive 控制,避免在模板里硬编码 |
| 组件名 | 每个页面组件都定义 name,且和路由 name 保持一致 |
| keep-alive 位置 | 放在实际渲染页面的 router-view 外层(多级路由时注意层级) |
| 列表页 | 在 activated 中按需刷新,用 deactivated 保存滚动位置 |
| 登录 / 404 等 | 一律 meta.keepAlive: false 或加入 exclude |
[⬆ 返回目录](#⬆ 返回目录)
九、小结
keep-alive 的核心就是三件事:
- include / exclude :通过组件
name决定缓存范围; - meta.keepAlive:用路由配置统一管理,方便维护;
- activated / deactivated:在缓存组件里做刷新和状态恢复。
记住:组件必须写 name,否则 include / exclude 不会生效。结合路由 meta 和生命周期,就可以把缓存控制得清晰、可控。
[⬆ 返回目录](#⬆ 返回目录)
🔍 系列模块导航
📝 状态管理与路由规范
一、《Vue3 Pinia 状态管理规范:状态拆分、Actions 写法、持久化实战,避坑状态污染|状态管理与路由规范篇》
二、《Vue3 Pinia 状态管理规范:何时用 Pinia 何时用本地状态|状态管理与路由规范篇》
三、《Vue Router 实战规范:path/name/meta 配置 + 动态 / 嵌套路由,统一团队标准|状态管理与路由规范篇》
四、《Vue3 + Vue Router + Pinia 路由守卫规范:beforeEach 应做 / 不应做,避死循环、防重复请求|状态管理与路由规范篇》
五、《Vue keep-alive 实战避坑:include/exclude + 路由 meta 标记,中后台路由缓存精准可控|状态管理与路由规范篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
📚 系列总览
「前端规范实战系列 」正在持续更新中,后续会整理一篇《前端规范实战系列全系列目录导航》,包含每篇文章简介 + 直达链接,方便大家按顺序、体系化学习。
更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~