同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
一、先搞懂:多标签页为什么需要缓存?
1.1 典型场景
中后台系统常见结构:
┌─────────────────────────────────────────────────────────────┐
│ 首页 | 用户管理 | 订单列表 | 商品详情 | 数据报表 ← 多个 Tab
├─────────────────────────────────────────────────────────────┤
│ │
│ 【当前激活的页面内容区域】
│ │
└─────────────────────────────────────────────────────────────┘
无缓存时:
- 从「用户管理」切到「订单列表」,再切回「用户管理」
- 组件会被销毁再重建,之前的滚动位置、表单、筛选条件全部丢失
有缓存时:
- 切到其他页再回来,之前的滚动、表单、筛选等都保留
- 用户体验明显更好
所以多标签页场景下,通常要用 keep-alive 对路由组件做缓存。
二、keep-alive 到底是个啥?
2.1 一句话理解
<keep-alive> 是 Vue 内置组件,用来在切换时保留被包裹组件的状态(例如 data、滚动位置等),而不是销毁重建。
2.2 基本用法
<!-- App.vue 或 主布局组件 -->
<template>
<div id="app">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</template>
这里:
router-view渲染当前路由对应的组件keep-alive包裹component,让所有路由组件都被缓存
2.3 这样写会有什么问题?
如果 所有路由 都被缓存:
- 登录页、404 等不该缓存的页面也会被缓存
- 页面增多后,内存占用会变大
所以实际开发中,通常会用 include / exclude 或 路由 meta 来控制「哪些要缓存」。
三、include 和 exclude:精确控制缓存范围
3.1 基本概念
include:只有名字匹配的组件会被缓存exclude:名字匹配的组件不会被缓存
这里的「名字」指的是 组件的 name 选项,不是路由的 path 或 name。
3.2 静态写法示例
<template>
<router-view v-slot="{ Component }">
<!-- 只缓存 UserList 和 OrderList -->
<keep-alive include="UserList,OrderList">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
被缓存组件需要显式定义 name:
<!-- UserList.vue -->
<script>
export default {
name: 'UserList', // 必须和 include 里的名字一致
// ...
}
</script>
3.3 动态 include(配合路由 meta)
实际项目里,往往是「根据路由配置决定是否缓存」,而不是在 keep-alive 里写死组件名,所以会用到 路由 meta。
<!-- Layout.vue -->
<template>
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 假设 cachedViews 来自 pinia/vuex,存的是需要缓存的组件名
const cachedViews = computed(() => store?.cachedViews ?? [])
</script>
路由配置中标记「可缓存」并保证组件 name 正确:
// router/index.js
{
path: '/user/list',
name: 'UserList',
meta: { keepAlive: true }, // 表示该页面需要缓存
component: () => import('@/views/user/UserList.vue')
}
配合 Tab 逻辑:打开 Tab 时,把对应组件的 name 加入 cachedViews,关闭 Tab 时移除,这样 keep-alive 就能按需缓存。
3.4 include 的三种写法
<!-- 1. 字符串:多个用逗号分隔 -->
<keep-alive include="UserList,OrderList">
<component :is="Component" />
</keep-alive>
<!-- 2. 正则 -->
<keep-alive :include="/UserList|OrderList/">
<component :is="Component" />
</keep-alive>
<!-- 3. 数组 -->
<keep-alive :include="['UserList', 'OrderList']">
<component :is="Component" />
</keep-alive>
四、路由 meta 和 keep-alive 的配合
4.1 meta 的作用
路由 meta 用来放和业务相关的配置,比如:
keepAlive:是否缓存title:Tab 标题icon:图标affix:是否固定在 Tab 栏(如首页)
4.2 在路由里标记缓存
// router/index.js
export default [
{
path: '/home',
name: 'Home',
meta: {
title: '首页',
keepAlive: true,
affix: true // 首页固定,不能关闭
},
component: () => import('@/views/Home.vue')
},
{
path: '/user/list',
name: 'UserList',
meta: {
title: '用户管理',
keepAlive: true
},
component: () => import('@/views/user/UserList.vue')
},
{
path: '/login',
name: 'Login',
meta: {
title: '登录',
keepAlive: false // 登录页不缓存
},
component: () => import('@/views/Login.vue')
}
]
4.3 要点:组件 name 必须和路由 name 一致(或单独维护映射)
keep-alive 的 include 认的是 组件的 name,不是路由的 name。常见两种做法:
- 约定:路由
name和组件name一致 - 或:在 store 里维护「路由 name → 组件 name」的映射
否则即使 meta.keepAlive: true,组件也不会被缓存。
五、一个完整的多标签页 + 缓存示例
5.1 项目结构(Vue 3 + Vue Router 4)
src/
├── views/
│ ├── Home.vue
│ ├── UserList.vue
│ └── OrderList.vue
├── layout/
│ ├── Index.vue # 主布局(含 Tab 栏 + 内容区)
│ └── TabBar.vue # Tab 标签栏
├── store/
│ └── tabs.js # Tab 状态(Vuex / Pinia)
└── router/
└── index.js
5.2 路由配置
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('@/layout/Index.vue'),
redirect: '/home',
children: [
{
path: 'home',
name: 'Home',
meta: { title: '首页', keepAlive: true, affix: true },
component: () => import('@/views/Home.vue')
},
{
path: 'user/list',
name: 'UserList',
meta: { title: '用户管理', keepAlive: true },
component: () => import('@/views/user/UserList.vue')
},
{
path: 'order/list',
name: 'OrderList',
meta: { title: '订单列表', keepAlive: true },
component: () => import('@/views/order/OrderList.vue')
}
]
}
]
export default createRouter({
history: createWebHistory(),
routes
})
5.3 Tab 状态管理(Pinia 示例)
// store/tabs.js
import { defineStore } from 'pinia'
export const useTabsStore = defineStore('tabs', {
state: () => ({
visitedViews: [], // 已访问的 Tab 列表
cachedViews: [] // 需要缓存的组件 name 列表(给 keep-alive 用)
}),
actions: {
addView(view) {
// 避免重复
if (this.visitedViews.some(v => v.path === view.path)) return
this.visitedViews.push({
name: view.name,
path: view.path,
title: view.meta?.title || '未命名'
})
// 需要缓存的加入 cachedViews
if (view.meta?.keepAlive && !this.cachedViews.includes(view.name)) {
this.cachedViews.push(view.name)
}
},
removeView(view) {
const index = this.visitedViews.findIndex(v => v.path === view.path)
if (index > -1) {
this.visitedViews.splice(index, 1)
const cacheIndex = this.cachedViews.indexOf(view.name)
if (cacheIndex > -1) {
this.cachedViews.splice(cacheIndex, 1)
}
}
}
}
})
5.4 主布局(含 keep-alive)
<!-- layout/Index.vue -->
<template>
<div class="layout">
<TabBar :tabs="tabsStore.visitedViews" @close="handleCloseTab" />
<main class="main-content">
<router-view v-slot="{ Component, route }">
<keep-alive :include="tabsStore.cachedViews">
<component
:is="Component"
:key="route.fullPath"
/>
</keep-alive>
</router-view>
</main>
</div>
</template>
<script setup>
import { watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import TabBar from './TabBar.vue'
import { useTabsStore } from '@/store/tabs'
const route = useRoute()
const router = useRouter()
const tabsStore = useTabsStore()
// 路由变化时,往 Tab 里加一条
watch(
() => route,
(newRoute) => {
tabsStore.addView(newRoute)
},
{ immediate: true }
)
function handleCloseTab(tab) {
tabsStore.removeView(tab)
// 如果关闭的是当前页,跳转到最后一个 Tab
if (route.path === tab.path) {
const last = tabsStore.visitedViews[tabsStore.visitedViews.length - 1]
router.push(last?.path || '/home')
}
}
</script>
5.5 被缓存组件的 name 定义
<!-- views/UserList.vue -->
<template>
<div class="user-list">
<!-- 表格、搜索、分页等 -->
</div>
</template>
<script>
export default {
name: 'UserList', // 必须和路由 name 一致,否则 include 不生效
// ...
}
</script>
Home.vue、OrderList.vue 同理,name 要和对应路由的 name 保持一致。
六、常见坑点
6.1 坑一:组件没定义 name,或 name 和 include 不一致
- 现象:写了
keepAlive: true,但切换回来还是重新加载 - 原因:
include匹配的是组件的name,未定义或名字对不上,就不会被缓存 - 解决:为需要缓存的组件写
name,并和路由name(或你维护的cachedViews列表)一致
6.2 坑二:动态组件没有 key 或 key 不合适
- 现象:同路由不同参数(如
/user/1、/user/2)共用一个缓存 - 原因:
component没有合适的key,Vue 会复用同一个实例 - 解决:用
route.fullPath作为 key,让「路径+参数」不同时使用不同缓存
<component :is="Component" :key="route.fullPath" />
6.3 坑三:关闭 Tab 后缓存未清除
- 现象:Tab 关了,但组件仍被 keep-alive 缓存,内存不释放
- 原因:
cachedViews没有在关 Tab 时移除对应组件名 - 解决:在
removeView中同时从cachedViews移除该组件的name
6.4 坑四:使用了 Vue Router 的 <router-view> 嵌套
- 现象:子路由页面不缓存
- 原因:嵌套路由下,缓存的是父级组件,子路由组件在父级内部
- 解决:把
keep-alive包在真正渲染子路由的router-view外层,并确保子组件也有正确的name
七、生命周期:activated 和 deactivated
使用 keep-alive 后,组件不会被销毁,所以 mounted 只会执行一次。需要「每次显示时」做刷新时,用 activated;需要在「每次离开时」做清理时,用 deactivated。
<script>
export default {
name: 'UserList',
activated() {
// 每次从其他 Tab 切回来时触发
this.fetchData()
},
deactivated() {
// 每次切换到其他 Tab 时触发
// 可做清理,如取消请求、清除定时器等
}
}
</script>
八、小结
| 概念 | 作用 |
|---|---|
| keep-alive | 缓存组件实例,避免重复创建销毁 |
| include | 指定要缓存的组件名(字符串/正则/数组) |
| exclude | 指定不缓存的组件名 |
| meta.keepAlive | 在路由中标记该页面是否参与缓存 |
| 组件 name | keep-alive 通过它匹配 include/exclude,必须正确设置 |
多标签页场景的典型流程:
- 在路由
meta里标记keepAlive: true - 用 Tab 逻辑维护
cachedViews(组件名列表) - 用
keep-alive :include="cachedViews"控制缓存 - 给需要缓存的组件设置正确的
name - 需要「每次进入时」刷新时,在
activated里写逻辑
按这个思路实现,就能得到一个可用的多标签页 + 缓存方案,并避免大部分常见问题。如果你希望我帮你再细化某一块(比如 TabBar 关闭逻辑、权限和 affix 的交互),可以说一下具体场景,我可以再补一版更贴近你项目的示例。
🔍 本系列专栏导航
一、《路由与布局骨架篇:Vue Router 实战 | 动态路由、嵌套路由与多级菜单》
二、《路由与布局骨架篇:登录态与路由守卫 | token 校验、白名单、重定向》
三、《路由与布局骨架篇:多标签页(Tab)与缓存 | keep-alive、includeexclude、路由 meta》
四、《路由与布局骨架篇:布局系统 | 头部、侧边栏、内容区、面包屑的拆分与复用》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~