背景与问题
在构建中后台管理系统时,动态菜单(Permission Menu)是标准功能。通常的实现流程是:
- 用户登录,获取 Token。
- 进入主页,调用用户信息接口(
/api/user/permissions)。 - 后端返回权限列表和菜单数据。
- 前端根据数据动态生成路由和菜单树。
- 渲染侧边栏组件。
痛点 : 在这个流程中,步骤 2 和 3 是异步网络请求。在请求完成前,菜单数据为空,导致侧边栏会出现短暂的空白 或Loading 状态。 对于用户体验来说,每次刷新页面或重新进入系统,都要忍受 200ms - 1s 的菜单"闪烁"或"白屏",这极大地影响了系统的流畅感。
优化方案:Cache-First + 增量更新
为了解决这个问题,我们采用了类似 PWA 的 Cache-First(缓存优先) 策略,结合 增量更新(Incremental Update) 机制。
核心策略
-
缓存优先渲染 (Cache-First Rendering):
- 页面初始化时,不等待 网络请求,直接从
localStorage读取上一次缓存的菜单数据进行渲染。 - 结果:用户看到的菜单是"瞬间"加载的,零延迟。
- 页面初始化时,不等待 网络请求,直接从
-
静默后台更新 (Silent Background Update):
- 在缓存渲染完成后,立即在后台发起用户信息请求。
- 这是一个"静默"操作,用户无感知。
-
增量更新检测 (Incremental Update Check):
- 获取到最新数据后,使用深度比较(Deep Compare)算法对比新旧数据。
- 如果数据一致:什么都不做。避免 Vue 触发无意义的 DOM 重绘(Re-render)。
- 如果数据变更:更新缓存和响应式数据,Vue 自动更新视图。
技术实现
1. 增量更新逻辑 (CachePermissions.ts)
利用 lodash-es 的 isEqual 进行深度比较,确保只在真正需要时更新缓存。
typescript
// src/composables/cache/CachePermissions.ts
import { isEqual } from 'lodash-es'
export function setCachePermissions(userInfo: UserInfoWithPermissions): void {
// ... 数据预处理 ...
// 1. 用户信息增量更新
const cachedUserInfo = webCache.get(CACHE_KEY.USER)
if (!isEqual(cachedUserInfo, userInfo)) {
webCache.set(CACHE_KEY.USER, userInfo)
}
// ... 构建菜单树 ...
const sortedMenuTree = sortMenuTree(menuTree)
// 2. 菜单树增量更新
const cachedMenuTree = webCache.get(CACHE_KEY.ROLE_ROUTERS)
if (!isEqual(cachedMenuTree, sortedMenuTree)) {
webCache.set(CACHE_KEY.ROLE_ROUTERS, sortedMenuTree)
console.log('[Permission] 菜单数据已更新')
} else {
console.log('[Permission] 菜单数据无变更,跳过更新')
}
}
2. 组件侧渲染策略 (MainMenu.vue)
组件初始化时采用同步读取缓存 + 异步更新的模式。
typescript
// src/layout/components/MainMenu/src/MainMenu.vue
/**
* 从缓存加载并构建菜单
*/
function loadMenusFromCache() {
const localRouters = webCache.get(CACHE_KEY.ROLE_ROUTERS)
// ... 构建菜单 ViewModel ...
// Vue 的响应式系统会自动处理 Diff,但这里我们只在数据变动时赋值更好
// 或者依赖 Vue 3 高效的 Virtual DOM Diff
menuList.value = finalMenuList
}
/**
* 初始化用户数据和菜单
* @description 采用"优先缓存,后台更新"策略
*/
async function initUserStoreAndMenus(): Promise<void> {
// 1. 【关键】立即从缓存加载菜单,消除白屏
loadMenusFromCache()
// 2. 异步获取最新数据 (静默更新)
try {
await userStore.setUserInfoAction()
// 3. 数据更新后,重新加载
// 由于 setCachePermissions 做了增量检测,如果数据没变,
// webCache.get 获取的引用可能没变(取决于 storage 实现),
// 即使变了,Vue 的 diff 也能处理,但最重要的是避免了数据抖动
loadMenusFromCache()
} catch (e) {
console.warn('用户信息同步失败,降级使用缓存')
}
}
// 立即执行
initUserStoreAndMenus()
优化效果
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 首屏菜单可见时间 | 300ms - 800ms (取决于网络) | 0ms (即时) | ∞ |
| 视觉体验 | 先空白/Loading -> 闪烁 -> 显示 | 稳定显示,无闪烁 | 显著提升 |
| 网络依赖 | 强依赖,请求失败菜单不显示 | 弱依赖,离线也能看菜单 | 可靠性提升 |
| 渲染性能 | 每次刷新必重绘 | 仅数据变更时重绘 | 减少资源消耗 |
总结
对于读多写少 (Read-heavy, Write-rarely)的数据,如菜单、字典、配置项,"缓存优先 + 增量更新" 是提升用户体验的黄金法则。它将网络延迟从用户感知的关键路径(Critical Path)中移除,让 Web 应用拥有了原生应用般的流畅度。