{T}

菜单高亮持久化、面包屑过渡与 KeepAlive 页面缓存

概述

这一节把后台布局的导航体验收口到“一套路由上下文、多种表现层”的层次上:左侧菜单刷新后保持高亮、Header 接入带过渡的面包屑、面包屑下方增加标签页式快速切换区、内容区加轻量切换动画,并对高频页面启用 KeepAlive 缓存。菜单高亮、面包屑、标签页本质上都是同一套路由状态的不同投影,关键不在于各自维护一份状态,而在于统一从路由推导。

学习目标

  • 掌握刷新后菜单高亮的恢复机制:统一从 route.pathroute.meta.activeMenu 推导激活项。
  • 理解主题色从局部样式升级为全局 CSS 变量后,如何实现系统级联动。
  • watch(..., { immediate: true }) 解决面包屑“刷新后不显示”的初始化时序问题。
  • 区分 transitionname 命名空间,以及 transitiontransition-group 的适用边界。
  • KeepAlive 缓存高频切换的复杂页面,正确解读 DevTools 里的 inactive 实例。

一、菜单高亮持久化是“导航状态恢复”,不是样式修补

用户进入某个菜单页后刷新浏览器,高亮仍能保留。关键不是给菜单项补一个 active class,而是让激活态继续和当前路由保持一致——只要“当前页面是谁”能从路由或菜单树稳定推导,刷新后高亮自然恢复。

ts
const activeMenu = computed(() => (route.meta?.activeMenu as string) || route.path)
Vue SFC
<el-menu :default-active="activeMenu" />

菜单高亮最好统一从路由或菜单元数据推导,不要额外维护一份容易失真的本地状态。详情页、编辑页这类“页面路径和菜单路径不一致”的场景,应使用 route.meta.activeMenu。刷新恢复通常要和“父级 SubMenu 自动展开”一起设计,否则会出现“高亮对了但层级没展开”的割裂体验。

二、主题色应作为全局状态,而非散落的局部样式

把主题色切到绿色后,页面多处视觉元素整体同步成绿色,而不是只改一个按钮。这说明主题色不应散落在多个组件局部样式里,而应作为全局布局配置的一部分,由 Header、菜单、激活态、标签页共同消费。

ts
const primaryColor = ref("#18a058")
document.documentElement.style.setProperty("--app-primary-color", primaryColor.value)
css
:root {
  --app-primary-color: #409eff;
}
.is-active {
  color: var(--app-primary-color);
}

主题色应尽量收口为 CSS 变量或统一状态源。若直接改 Element Plus 相关颜色,要同步考虑 hover、active、light 系列状态色。主题色影响范围越大,越需要提前梳理“哪些区域服从主题色、哪些保持品牌色或语义色”。

三、面包屑初始化必须 immediate,否则刷新后不出现

面包屑数据不能只等用户切路由再更新,组件第一次进入时也要立刻生成当前页面对应的面包屑。若通过 watch 监听 route.matched 生成面包屑,应开启 immediate: true,否则常见现象就是刷新后页面已在目标路由,面包屑却要等下一次切换才出现。

ts
watch(
  () => route.matched,
  () => updateBreadcrumbs(),
  { immediate: true }
)
ts
function updateBreadcrumbs() {
  breadcrumbs.value = route.matched
    .filter((item) => item.meta?.title)
    .map((item, index) => ({ index, title: item.meta?.title as string, path: item.path, name: item.name }))
}

四、transitionname 是类名命名空间,不是装饰字段

<transition name="breadcrumb"> 后,Vue 会自动拼接出 breadcrumb-enter-frombreadcrumb-leave-active 这一整套类名,前缀不再是默认的 v-。所以一旦设了 name,CSS 里就必须同步改成对应前缀,否则样式不命中。面包屑容器还应绑定稳定的 key(常用 route.path),配合 mode="out-in" 产生平滑的方向性过渡。

Vue SFC
<transition mode="out-in" name="breadcrumb">
  <el-breadcrumb :key="route.path">
    <el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
      {{ item.title }}
    </el-breadcrumb-item>
  </el-breadcrumb>
</transition>

五、面包屑分隔符与跳转应来自真实路由语义

两个易被忽略的细节:一是把默认分隔符换成右箭头图标(separator-icon 接收的是组件,TSX 里先定义函数组件再传入);二是点击跳转优先用命名路由 name,不要直接拿嵌套路由记录的局部 path——嵌套路由里局部 path 不一定是完整跳转目标。

Vue SFC
<el-breadcrumb :separator-icon="ArrowRight">
  <el-breadcrumb-item
    v-for="item in breadcrumbs"
    :key="item.name || item.path"
    :to="item.name ? { name: item.name } : item.path"
  >
    {{ item.title }}
  </el-breadcrumb-item>
</el-breadcrumb>

命名路由导航的前提是每一级可点击面包屑都配置了稳定、可用的 name;否则需要在生成面包屑时额外拼接完整路径。

六、父级路由要有 redirect,模块级面包屑才能回到首页

点击面包屑里的模块名,预期回到该模块默认展示页。若父级路由没有默认子组件也没有 redirect,点击后就会停在一个不完整的父容器路由上。常见做法是给父级路由加 redirect 指向模块首页,并用 order 控制菜单里默认页排序。若模块首页是权限相关或动态配置页面,则不能在静态路由里写死,需在导航守卫或数据加载后决定落点。

七、transitiontransition-group 的边界,以及 KeepAlive

面包屑切换本质是“单个容器方向性过渡”,更适合 transitiontransition-group 处理一组元素(列表增删、排序、位移),关键是额外的 *-move 类描述位置变更,列表删除时还需给 *-leave-activeposition: absolute 让兄弟元素顺畅补位。不要为了“看起来高级”硬上 transition-group

内容区切换动画的价值是柔化复杂组件的感知跳变,但动画本身不是性能优化。真正减少重复渲染的是 KeepAlive

Vue SFC
<RouterView v-slot="{ Component, route: currentRoute }">
  <transition mode="out-in" name="fade-slide">
    <KeepAlive>
      <component :is="Component" :key="currentRoute.fullPath" />
    </KeepAlive>
  </transition>
</RouterView>

KeepAlive 缓存高频切换、初始化较重的页面,避免重复销毁重建。DevTools 里大量 inactive 实例不是异常,而是缓存生效的可视化证据——当前页 active,已访问但被缓存的页 inactive。需要关注缓存粒度:缓存页面过多时要检查 include / exclude 或标签页关闭后的清理策略。

八、面包屑下方增加标签页式快捷导航区

在面包屑下方增加类似 Tabs 的切换区,给当前模块高频页面提供更直接的切换入口,并和左侧菜单共享同一路由状态源。标签激活态不要和菜单各管各的,二者都从当前路由推导:

ts
const activeTab = computed({
  get: () => route.path,
  set: (path: string) => router.push(path)
})

标签切换和左侧高亮不同步,通常说明状态源分裂了。


常见问题

问题原因解决方案
刷新后左侧菜单没保持高亮激活态只在点按时更新,没和路由恢复联动统一从 route.pathroute.meta.activeMenu 推导
刷新后面包屑不显示生成逻辑只监听变化,没在初始化立即执行watch(..., { immediate: true }) 或挂载时先同步一次
自定义了 transitionname,动画却不生效CSS 还在写默认 v-enter-* 类名改成 breadcrumb-enter-from 等对应前缀
点击面包屑跳错或路径不完整直接用了嵌套路由局部 path优先用 item.name 命名路由;无 name 时拼完整路径
点模块级面包屑回不到首页父级路由缺默认子页、redirect给父级路由加 redirect 明确落点
列表增删时周围元素突兀跳动只写 enter/leave,没处理补位transition-group*-move,必要时 *-leave-active 绝对定位
点标签页后左侧高亮不同步标签页与菜单状态源分裂统一以路由为导航状态源
页面切换仍卡顿只加动画没缓存页面高频复杂页用 KeepAlive
DevTools 大量 inactive 实例KeepAlive 缓存后失活的正常状态结合缓存策略判断是否需清理或限范围

延伸阅读