版本: v2.0 创建日期: 2025-12-10 标签: Vue3, Keep-Alive, 性能优化, 用户体验
📖 引言
在企业级中后台应用中,用户经常需要在列表页和详情页之间频繁切换。每次返回列表页都要重新加载数据、丢失查询条件,这种体验让人抓狂。本文将深入探讨 Vue 3 的 keep-alive 机制,并分享我们在 CMC Link IBS 项目中的完整实践方案。
阅读本文你将学到:
- 🎯 keep-alive 的工作原理和核心概念
- 🛠️ 如何设计一套完整的页面缓存管理方案
- 📊 真实项目中的性能收益数据
- ⚠️ 常见坑点和解决方案
一、是什么:Keep-Alive 核心概念
1.1 什么是 Keep-Alive?
<keep-alive> 是 Vue 内置的抽象组件,用于缓存不活动的组件实例,而不是销毁它们。当组件在 <keep-alive> 内被切换时:
普通组件切换流程:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 组件 A │ ──▶ │ 销毁 A │ ──▶ │ 创建 B │
└─────────┘ └─────────┘ └─────────┘
keep-alive 组件切换流程:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 组件 A │ ──▶ │ 缓存 A │ ──▶ │ 创建 B │
└─────────┘ └─────────┘ └─────────┘
│
▼ 返回时
┌─────────┐
│ 恢复 A │ ← 直接从缓存恢复,无需重新创建
└─────────┘
1.2 生命周期变化
使用 keep-alive 后,组件会获得两个新的生命周期钩子:
| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
onActivated | 组件被激活(从缓存恢复) | 刷新数据、恢复滚动位置 |
onDeactivated | 组件被停用(进入缓存) | 保存状态、清理定时器 |
// Vue 3 Composition API
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
console.log('页面从缓存恢复')
})
onDeactivated(() => {
console.log('页面进入缓存')
})
1.3 include/exclude 匹配机制
keep-alive 通过 include 属性控制哪些组件需要缓存,匹配规则是:组件的 name 属性。
<!-- 只缓存名为 ListView 和 DetailView 的组件 -->
<keep-alive :include="['ListView', 'DetailView']">
<router-view />
</keep-alive>
⚠️ 重要:Vue 3 <script setup> 组件默认没有 name,必须使用 defineOptions 显式定义:
// ❌ 错误:没有定义 name,keep-alive 无法匹配
<script setup lang="ts">
// ...
</script>
// ✅ 正确:使用 defineOptions 定义组件名称
<script setup lang="ts">
defineOptions({
name: 'ListView', // 必须与路由 name 一致
})
</script>
二、为什么:业务痛点与价值分析
2.1 用户体验痛点
在没有页面缓存的情况下,用户会遇到以下问题:
| 场景 | 痛点 | 用户感受 |
|---|---|---|
| 列表→详情→返回 | 列表重新加载,滚动位置丢失 | 😤 每次都要重新翻页 |
| 复杂查询条件 | 返回后条件全部丢失 | 😤 又要重新选一遍 |
| 大数据量表格 | 每次进入等待 1-2 秒 | 😤 系统好慢 |
| Tab 切换 | 切换后数据重新加载 | 😤 明明刚看过 |
2.2 性能收益预期
基于我们的实测数据:
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 页面切换时间 | 800-1200ms | < 50ms | 95%+ |
| API 请求次数 | 每次进入都请求 | 仅首次请求 | 70%+ |
| 用户等待感知 | 明显等待 | 瞬间切换 | 体验质变 |
2.3 业务价值
- 效率提升:操作人员每天处理数百单,减少等待时间直接提升工作效率
- 服务器压力降低:减少 70% 的重复 API 请求
- 用户满意度:流畅的操作体验提升系统好评度
三、怎么做:完整实现方案
3.1 架构设计
┌─────────────────────────────────────────────────────────────┐
│ 缓存管理架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 路由配置 │ │ KeepAlive │ │ usePageCache │ │
│ │ meta.keepAlive│───▶│ Store │◀───│ Composable │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Layout 组件 │ │
│ │ <keep-alive :include="cachedViews" :max="15"> │ │
│ │ <router-view /> │ │
│ │ </keep-alive> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
3.2 核心代码实现
步骤 1:创建 KeepAlive Store
// src/store/core/keepAlive.ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useKeepAliveStore = defineStore('keep-alive', () => {
// 缓存的组件名称列表
const cachedViews = ref<string[]>([])
// 最大缓存数量
const MAX_CACHE_SIZE = 15
// 添加缓存
function addCachedView(name: string): void {
if (!name || cachedViews.value.includes(name))
return
// 超过最大数量时,移除最早的
if (cachedViews.value.length >= MAX_CACHE_SIZE) {
cachedViews.value.shift()
}
cachedViews.value.push(name)
}
// 移除缓存
function deleteCachedView(name: string): void {
const index = cachedViews.value.indexOf(name)
if (index > -1) {
cachedViews.value.splice(index, 1)
}
}
// 清空所有缓存(用于登出)
function clearAllCachedViews(): void {
cachedViews.value = []
}
return {
cachedViews: computed(() => cachedViews.value),
addCachedView,
deleteCachedView,
clearAllCachedViews,
}
})
步骤 2:配置 Layout 组件
<!-- src/layout/index.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import { useKeepAliveStore } from '~/store/core/keepAlive'
const keepAliveStore = useKeepAliveStore()
const cachedViews = computed(() => keepAliveStore.cachedViews)
</script>
<template>
<div class="layout">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<keep-alive :include="cachedViews" :max="15">
<component :is="Component" :key="$route.name" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<style scoped>
/* 页面切换过渡动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
步骤 3:路由守卫自动管理
// src/router/index.ts
router.afterEach((to, from) => {
const keepAliveStore = useKeepAliveStoreWithOut()
const componentName = to.name as string
// 根据路由 meta.keepAlive 自动添加到缓存
if (to.meta?.keepAlive && componentName) {
keepAliveStore.addCachedView(componentName)
}
// 如果离开的页面配置了 noCache,则清除缓存
if (from.meta?.noCache && from.name) {
keepAliveStore.deleteCachedView(from.name as string)
}
})
步骤 4:创建 usePageCache Composable
// src/composables/cache/usePageCache.ts
import { onActivated, onDeactivated, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useKeepAliveStoreWithOut } from '~/store/core/keepAlive'
// 刷新标记(跨页面通信)
const refreshMarks = new Set<string>()
// 标记某个页面需要刷新
export function markPageNeedRefresh(pageName: string): void {
refreshMarks.add(pageName)
}
// 页面缓存管理 Composable
export function usePageCache(options: {
refreshOnActivate?: boolean
onActivate?: () => void
onDeactivate?: () => void
onRefresh?: () => void | Promise<void>
} = {}) {
const route = useRoute()
const keepAliveStore = useKeepAliveStoreWithOut()
const isActive = ref(true)
const needRefresh = ref(false)
const currentPageName = route.name as string
onActivated(() => {
isActive.value = true
// 检查是否被标记需要刷新
if (currentPageName && refreshMarks.has(currentPageName)) {
needRefresh.value = true
refreshMarks.delete(currentPageName)
}
// 执行刷新回调
if ((options.refreshOnActivate || needRefresh.value) && options.onRefresh) {
Promise.resolve(options.onRefresh()).catch(console.error)
needRefresh.value = false
}
options.onActivate?.()
})
onDeactivated(() => {
isActive.value = false
options.onDeactivate?.()
})
// 从缓存中移除当前页面
function removeFromCache(): void {
if (currentPageName) {
keepAliveStore.deleteCachedView(currentPageName)
}
}
return { isActive, needRefresh, removeFromCache }
}
3.3 业务页面接入
<!-- 列表页示例 -->
<script setup lang="ts">
import { onMounted } from 'vue'
import { usePageCache } from '~/composables/cache'
// !重要:必须定义组件名称
defineOptions({
name: 'MyApply', // 与路由 name 保持一致
})
// 集成缓存管理
const { isActive } = usePageCache({
onActivate: () => {
console.log('[MyApply] 页面激活')
},
onRefresh: async () => {
// 被标记刷新时执行
await loadList()
},
})
// 首次加载
onMounted(() => {
loadList()
})
</script>
四、当前状态分析
已配置缓存的页面(11个)
| 模块 | 页面 | 路由名称 | 缓存状态 |
|---|---|---|---|
| 核心 | 工作台 | Dashboard | ✅ keepAlive: true |
| 核心 | 用户中心 | UserCenter | ✅ keepAlive: true |
| 核心 | 工作台 | Workbench | ✅ keepAlive: true |
| 核心 | 订阅消息 | SubscriptionMessage | ✅ keepAlive: true |
| 搜索服务 | 快速查询 | QuickSearch | ✅ keepAlive: true |
| 搜索服务 | 我的船期 | MyShipSchedule | ✅ keepAlive: true |
| 搜索服务 | 货物跟踪 | FreightTrack | ✅ keepAlive: true |
| 出口业务 | 订舱管理 | BookingManage | ✅ keepAlive: true |
| 出口业务 | 提单管理 | LadingManagement | ✅ keepAlive: true |
| 出口业务 | 签单打印 | LadingSignPrint | ✅ keepAlive: true |
| 出口业务 | 舱单管理 | ManifestManage | ✅ keepAlive: true |
| 进口业务 | SI管理 | ImportSiManage | ✅ keepAlive: true |
未配置缓存的列表页(需评估)
| 模块 | 页面 | 路由名称 | 建议 | 优先级 |
|---|---|---|---|---|
| 箱管 | 出口放箱列表 | ExportBoxPlacement | ⭐ 建议缓存 | P1 |
| 箱管 | 进口放箱列表 | ImportBoxPlacement | ⭐ 建议缓存 | P1 |
| 箱管 | 我的放箱 | MyBoxPlacement | ⭐ 建议缓存 | P1 |
| 箱管 | 我的申请 | MyApply | ⭐ 建议缓存 | P1 |
| 支付 | 付款账户 | PaymentAccount | ⭐ 建议缓存 | P2 |
| 支付 | 发票管理 | InvoiceManage | ⭐ 建议缓存 | P2 |
| 支付 | 付款管理 | PayManage | ⭐ 建议缓存 | P2 |
| 支付 | 箱押金管理 | ContainerDeposit | ⭐ 建议缓存 | P2 |
| 支付 | 押金退款 | ContainerDepositRefund | ⭐ 建议缓存 | P2 |
| 单证 | 单证核查 | DocumentVerification | ❓ 视情况 | P3 |
不建议缓存的页面(详情/编辑页)
| 模块 | 页面 | 原因 |
|---|---|---|
| 箱管 | 出口放箱详情 | 需要最新数据 |
| 箱管 | 进口放箱详情 | 需要最新数据 |
| 箱管 | 超期减免申请 | 表单页,提交后应清空 |
| 箱管 | 特殊用箱申请 | 表单页,提交后应清空 |
| 出口 | 创建提单 | 表单页 |
| 出口 | 提单拆分/合并 | 操作页 |
二、缓存管理策略
2.1 页面分类
┌─────────────────────────────────────────────────────────────┐
│ 页面缓存策略矩阵 │
├─────────────────┬───────────────────┬───────────────────────┤
│ 页面类型 │ 缓存策略 │ 刷新策略 │
├─────────────────┼───────────────────┼───────────────────────┤
│ 列表页(高频) │ keepAlive: true │ 返回时保持,可手动刷新 │
│ 列表页(低频) │ keepAlive: false │ 每次进入重新加载 │
│ 详情页 │ keepAlive: false │ 每次进入重新加载 │
│ 编辑/表单页 │ noCache: true │ 离开时清除缓存 │
│ Dashboard │ keepAlive: true │ 定时自动刷新 │
└─────────────────┴───────────────────┴───────────────────────┘
2.2 使用 usePageCache 的场景
// 场景1:列表页返回时自动刷新(如详情页操作后)
// 场景3:详情页操作后通知列表页刷新
import { markPageNeedRefresh } from '~/composables/cache'
usePageCache({
refreshOnActivate: false, // 不自动刷新
onRefresh: () => fetchList(), // 被标记刷新时执行
})
// 场景2:需要监听激活/停用的页面
usePageCache({
onActivate: () => console.log('页面激活'),
onDeactivate: () => console.log('页面停用'),
})
markPageNeedRefresh('LadingManagement') // 标记提单管理需要刷新
三、任务清单
Phase 1: 试点验证(P1 - 本周)
-
任务 1.1: 选择试点页面
MyApply(我的申请)- 添加
keepAlive: true配置 - 集成
usePageCachecomposable - 添加性能监控代码
- 添加
-
任务 1.2: 性能基准测试
- 记录优化前页面加载时间
- 记录优化前 API 请求次数
- 记录用户操作流畅度指标
-
任务 1.3: 验收与对比
- 对比优化前后性能数据
- 验证返回后数据状态正确性
- 测试手动刷新功能
Phase 2: 箱管模块推广(P1 - 下周)
- 任务 2.1: 出口放箱列表(ExportBoxPlacement)
- 任务 2.2: 进口放箱列表(ImportBoxPlacement)
- 任务 2.3: 我的放箱(MyBoxPlacement)
- 任务 2.4: 详情页返回刷新联动
Phase 3: 支付结算模块(P2)
- 任务 3.1: 付款账户(PaymentAccount)
- 任务 3.2: 发票管理(InvoiceManage)
- 任务 3.3: 付款管理(PayManage)
- 任务 3.4: 箱押金相关页面
Phase 4: 全量评估与优化(P3)
- 任务 4.1: 收集各页面性能数据
- 任务 4.2: 调整缓存最大数量配置
- 任务 4.3: 优化内存占用
四、性能收益预期
4.1 预期收益
| 指标 | 优化前 | 预期优化后 | 提升幅度 |
|---|---|---|---|
| 列表页切换时间 | 800-1200ms | 50-100ms | 90%+ |
| API 重复请求 | 每次进入 | 仅首次/标记刷新 | 70%+ |
| 用户等待时间 | 明显感知 | 无感知 | 体验提升 |
| 查询条件保持 | 丢失 | 保持 | 便利性提升 |
4.2 潜在风险
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| 数据过期 | 用户看到旧数据 | 提供刷新按钮 + 标记刷新机制 |
| 内存占用 | 缓存过多页面 | 限制最大缓存数(当前15) |
| 状态残留 | 表单数据残留 | 详情/表单页不缓存 |
五、试点页面实施指南
5.1 MyApply 页面改造示例
// src/views/equipment_control/my_apply/index.vue
import { markPageNeedRefresh, usePageCache } from '~/composables/cache'
// 1. 集成缓存管理
const { isActive, needRefresh, removeFromCache } = usePageCache({
onRefresh: () => {
// 被标记需要刷新时执行
loadApplyList()
},
onActivate: () => {
console.log('[MyApply] 页面激活')
},
})
// 2. 手动刷新功能
function handleRefresh() {
loadApplyList()
}
5.2 路由配置修改
// src/router/modules/equipment-control.ts
{
path: RouteConfig.MyApply.path,
name: RouteConfig.MyApply.name,
component: async () => import('~/views/equipment_control/my_apply/index.vue'),
meta: {
title: RouteConfig.MyApply.title,
i18nKey: RouteConfig.MyApply.i18nKey,
keepAlive: true, // 添加缓存
},
},
六、性能监控代码
6.1 添加到试点页面
// 性能监控
const performanceMetrics = ref({
firstLoadTime: 0,
cacheHitCount: 0,
apiCallCount: 0,
})
onMounted(() => {
const start = performance.now()
loadApplyList().then(() => {
performanceMetrics.value.firstLoadTime = performance.now() - start
console.log(`[性能] 首次加载耗时: ${performanceMetrics.value.firstLoadTime.toFixed(2)}ms`)
})
})
onActivated(() => {
performanceMetrics.value.cacheHitCount++
console.log(`[性能] 缓存命中次数: ${performanceMetrics.value.cacheHitCount}`)
})
七、验收标准
试点验收(MyApply)
- 从列表页进入详情页后返回,查询条件和列表数据保持不变
- 从详情页操作(如编辑/删除)后返回,列表自动刷新
- 手动刷新按钮正常工作
- 切换 Tab 时数据正确加载
- 内存无明显泄漏
- 缓存命中时加载时间 < 100ms
八、常见问题与解决方案
8.1 缓存不生效?
症状:配置了 keepAlive: true,但页面仍然每次都重新加载
排查清单:
// 检查点 1:组件是否定义了 name?
defineOptions({
name: 'MyComponent', // 必须有
})
// 检查点 2:组件 name 是否与路由 name 一致?
// 路由配置
{ name: 'MyComponent', meta: { keepAlive: true } }
// 组件 name
defineOptions({ name: 'MyComponent' }) // 必须完全一致
// 检查点 3:Layout 组件是否正确配置?
<keep-alive :include="cachedViews">
<router-view />
</keep-alive>
8.2 数据过期怎么办?
场景:用户在详情页修改了数据,返回列表页希望看到最新数据
解决方案:使用 markPageNeedRefresh
// 详情页:保存成功后
import { markPageNeedRefresh } from '~/composables/cache'
async function handleSave() {
await saveData()
markPageNeedRefresh('ListPage') // 标记列表页需要刷新
router.back()
}
// 列表页:接收刷新通知
usePageCache({
onRefresh: () => loadList(), // 被标记时自动执行
})
8.3 内存泄漏风险?
风险:缓存过多页面导致内存占用过高
缓解措施:
- 限制最大缓存数:
<keep-alive :max="15"> - 登出时清空:
keepAliveStore.clearAllCachedViews() - 定期清理:LRU 策略自动淘汰最久未访问的页面
九、演示页面
我们提供了一个交互式演示页面,方便学习和调试:
访问路径:/develop/cache/keep-alive
功能特点:
- 实时性能指标监控
- 生命周期事件日志
- 模拟表单状态保持
- 缓存恢复耗时对比
验证步骤:
- 进入演示页面,填写表单数据
- 切换到其他页面(如工作台)
- 返回演示页面
- 观察:表单数据保持、激活次数增加、恢复耗时 < 50ms
十、最佳实践总结
应该做
- 列表页开启缓存:高频访问的列表页都应配置
keepAlive: true - 定义组件名称:所有缓存页面必须用
defineOptions({ name: 'xxx' })定义名称 - 提供刷新入口:每个缓存页面都应有手动刷新按钮
- 详情页触发刷新:详情页操作后使用
markPageNeedRefresh通知列表页
不应该做
- 表单页开启缓存:避免用户看到上次填写的数据
- 详情页开启缓存:详情数据应始终获取最新
- 无限缓存:必须限制最大缓存数量
- 忘记登出清理:用户登出时必须清空所有缓存
十一、附录
相关文件清单
| 文件 | 说明 |
|---|---|
src/store/core/keepAlive.ts | 缓存状态管理 Store |
src/composables/cache/usePageCache.ts | 页面缓存管理 Composable |
src/layout/index.vue | 主布局(keep-alive 配置) |
src/router/index.ts | 路由守卫(自动管理缓存) |
src/views/develop/demos/KeepAliveCacheDemo.vue | 演示页面 |
build/plugins/vite-plugin-auto-component-name.ts | Vite 插件(自动注册组件名) |
src/layout/index.vue | 主布局(自动监控) |
开发工作流
1. 配置路由 meta.keepAlive: true
↓
2. (可选)添加 defineOptions({ name })
└─ 如果启用了 Vite 插件,此步骤可省略
↓
3. 完成!缓存自动生效
Keep-Alive 缓存迁移指南
版本: v1.0
最后更新: 2025-12-10
适用范围: 所有配置了keepAlive: true的路由页面
❌ 不规范用法(禁止)
直接在业务组件中使用 onActivated / onDeactivated:
// ❌ 错误示例
import { onActivated } from 'vue'
onActivated(() => {
fetchList() // 每次激活都刷新
})
问题:
- 缺乏首次挂载的兜底逻辑
- 无法利用统一的刷新标记机制
- 无性能监控和缓存检测
- 代码不统一,维护成本高
✅ 规范用法
使用 usePageCache composable:
import { usePageCache } from '~/composables/cache'
// ✅ 正确示例
const { isActive, needRefresh } = usePageCache({
refreshOnActivate: true, // 或配置 onActivate 回调
onRefresh: () => fetchList()
})
迁移模式对照表
模式 1:每次激活都刷新
// ❌ 旧写法
onActivated(() => {
fetchAllData()
})
// ✅ 新写法
const { } = usePageCache({
refreshOnActivate: true,
onRefresh: () => fetchAllData()
})
模式 2:条件刷新(检查标记)
// ❌ 旧写法
onActivated(() => {
if (common.getWindowKeyValue(WindowKey.REFRESH_LIST)) {
common.setWindowKeyValue(WindowKey.REFRESH_LIST, null)
getPageData()
}
})
// ✅ 新写法 - 方案 A:使用内置刷新标记
// 在详情页操作后调用:markPageNeedRefresh('BookingManage')
const { } = usePageCache({
onRefresh: () => getPageData()
})
// ✅ 新写法 - 方案 B:保留自定义逻辑
const { } = usePageCache({
onActivate: () => {
if (common.getWindowKeyValue(WindowKey.REFRESH_LIST)) {
common.setWindowKeyValue(WindowKey.REFRESH_LIST, null)
getPageData()
}
}
})
模式 3:检查 initialized 后刷新
// ❌ 旧写法
onActivated(() => {
if (manageComposable.initialized.value) {
manageComposable.onSearch()
}
})
// ✅ 新写法
const { } = usePageCache({
onActivate: () => {
if (manageComposable.initialized.value) {
manageComposable.onSearch()
}
}
})
模式 4:首次挂载 + 激活刷新(避免重复)
// ❌ 旧写法(手动管理状态)
let isFirstMount = true
onMounted(() => {
fetchAllData()
})
onActivated(() => {
if (isFirstMount) {
isFirstMount = false
return
}
fetchAllData()
})
// ✅ 新写法(usePageCache 内部已处理)
const { } = usePageCache({
refreshOnActivate: true,
onRefresh: () => fetchAllData()
})
// 注:usePageCache 内部 onMounted 用于初始化监控,
// 首次数据加载应在组件顶层或 onRefresh 中处理
API 快速参考
interface UsePageCacheOptions {
/** 激活时是否自动刷新 @default false */
refreshOnActivate?: boolean
/** 刷新回调函数(refreshOnActivate=true 或被标记刷新时触发) */
onRefresh?: () => void | Promise<void>
/** 激活回调(每次激活都调用,用于自定义逻辑) */
onActivate?: () => void
/** 停用回调 */
onDeactivate?: () => void
}
// 返回值
const {
isActive, // 页面是否激活
needRefresh, // 是否需要刷新
metrics, // 性能指标(挂载次数、激活次数等)
markNeedRefresh, // 标记当前页面需要刷新
removeCache, // 移除指定页面缓存
removeSelfCache,// 移除当前页面缓存
clearAllCache, // 清空所有缓存
isCached, // 检查页面是否被缓存
} = usePageCache(options)
// 全局方法(可在任意地方调用)
markPageNeedRefresh('BookingManage') // 标记页面需要刷新
removePageCache('BookingManage') // 移除页面缓存
clearAllPageCache() // 清空所有缓存(登出时调用)
ESLint 规则
在 eslint.config.js 中添加自定义规则,禁止在路由组件中直接使用 onActivated:
// 参见 build/eslint/no-direct-activated.js
/**
* @file ESLint 规则:禁止在路由组件中直接使用 onActivated/onDeactivated
* @date 2025-12-10
* @description 强制使用 usePageCache composable 来处理 keep-alive 生命周期
*/
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: '禁止在路由组件中直接使用 onActivated/onDeactivated,请使用 usePageCache',
category: 'Best Practices',
recommended: true,
},
messages: {
noDirectActivated:
'❌ 禁止直接使用 {{ name }},请使用 usePageCache composable。\n'
+ ' 详见:src/composables/cache/MIGRATION.md\n'
+ ' 示例:const { } = usePageCache({ onActivate: () => { ... } })',
},
schema: [
{
type: 'object',
properties: {
// 允许的文件路径模式(用于白名单)
allowedPatterns: {
type: 'array',
items: { type: 'string' },
},
},
additionalProperties: false,
},
],
},
create(context) {
const options = context.options[0] || {}
const allowedPatterns = options.allowedPatterns || [
// 默认允许的路径(composable 内部使用)
'**/composables/**',
'**/hooks/**',
'**/develop/**', // 开发示例页面
]
// 检查文件路径是否在白名单中
const filename = context.getFilename()
const isAllowed = allowedPatterns.some((pattern) => {
// 简单的通配符匹配
const regex = new RegExp(
pattern
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*')
.replace(/\//g, '[\\\\/]'),
)
return regex.test(filename)
})
if (isAllowed) {
return {}
}
// 只检查 Vue 文件中的路由组件
if (!filename.endsWith('.vue')) {
return {}
}
// 检查是否在 views 目录下(路由组件)
const isRouteComponent = /[\\/]views[\\/]/.test(filename)
if (!isRouteComponent) {
return {}
}
return {
// 检查 import { onActivated } from 'vue'
ImportSpecifier(node) {
if (
node.imported
&& node.imported.type === 'Identifier'
&& (node.imported.name === 'onActivated' || node.imported.name === 'onDeactivated')
&& node.parent
&& node.parent.type === 'ImportDeclaration'
&& node.parent.source.value === 'vue'
) {
context.report({
node,
messageId: 'noDirectActivated',
data: { name: node.imported.name },
})
}
},
// 检查 onActivated() 调用
CallExpression(node) {
if (
node.callee.type === 'Identifier'
&& (node.callee.name === 'onActivated' || node.callee.name === 'onDeactivated')
) {
context.report({
node,
messageId: 'noDirectActivated',
data: { name: node.callee.name },
})
}
},
}
},
}
提示:本文档持续更新,如有问题或建议请联系作者。
最后更新:2025-12-10