Vue 3 页面缓存机制深度实践:从原理到落地

版本: 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< 50ms95%+
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 配置
    • 集成 usePageCache composable
    • 添加性能监控代码
  • 任务 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-1200ms50-100ms90%+
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 内存泄漏风险?

风险:缓存过多页面导致内存占用过高

缓解措施

  1. 限制最大缓存数:<keep-alive :max="15">
  2. 登出时清空:keepAliveStore.clearAllCachedViews()
  3. 定期清理:LRU 策略自动淘汰最久未访问的页面

九、演示页面

我们提供了一个交互式演示页面,方便学习和调试:

访问路径/develop/cache/keep-alive

功能特点

  • 实时性能指标监控
  • 生命周期事件日志
  • 模拟表单状态保持
  • 缓存恢复耗时对比

验证步骤

  1. 进入演示页面,填写表单数据
  2. 切换到其他页面(如工作台)
  3. 返回演示页面
  4. 观察:表单数据保持、激活次数增加、恢复耗时 < 50ms

十、最佳实践总结

应该做

  1. 列表页开启缓存:高频访问的列表页都应配置 keepAlive: true
  2. 定义组件名称:所有缓存页面必须用 defineOptions({ name: 'xxx' }) 定义名称
  3. 提供刷新入口:每个缓存页面都应有手动刷新按钮
  4. 详情页触发刷新:详情页操作后使用 markPageNeedRefresh 通知列表页

不应该做

  1. 表单页开启缓存:避免用户看到上次填写的数据
  2. 详情页开启缓存:详情数据应始终获取最新
  3. 无限缓存:必须限制最大缓存数量
  4. 忘记登出清理:用户登出时必须清空所有缓存

十一、附录

相关文件清单

文件说明
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.tsVite 插件(自动注册组件名)
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() // 每次激活都刷新
})

问题

  1. 缺乏首次挂载的兜底逻辑
  2. 无法利用统一的刷新标记机制
  3. 无性能监控和缓存检测
  4. 代码不统一,维护成本高

✅ 规范用法

使用 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