【前端+React+缓存竞态】导航切换缓存刷新机制

导航切换缓存刷新机制

功能概述

切换导航页面时,自动从两个 NE(网元)接口获取真实数据,内部页面使用缓存数据展示。同时提供请求去重、竞态条件防护、异常兜底等机制,确保页面在数据加载失败时不会白屏。


涉及文件

文件 角色
src/app/(main)/layout.tsx 入口:监听路由变化,主动获取两个接口数据并写入 store
src/store/store.ts 数据中心:定义缓存结构类型,提供全局状态
src/app/(main)/ne/neConfig/feature/ConfigDetail.tsx 消费者:从 store 读取缓存展示,并提供手动刷新能力

数据流架构

复制代码
路由变化 (pathname)
     │
     ▼
layout.tsx useEffect
     │
     ├─ ① 取消上一次请求 (fetchAbortRef.abort)
     │
     ├─ ② 创建新 AbortController + 递增 fetchId
     │
     ├─ ③ await neService.getNeData()
     │      └─ 重组排序:connected 在前,disconnected 在后
     │      └─ 写入 store.setNeData()
     │            └─ 触发 page.tsx → ConfigDetail 挂载
     │
     └─ ④ fetchId 核对 → getAllTypeData(ne.uid)
            └─ try-catch 保护
            └─ 写入 store.setAllTypeData({ uid, data })
                  └─ 触发 ConfigDetail 重新读取缓存

缓存读取流程(ConfigDetail)

复制代码
挂载 / typeActiveTabKey 变化
     │
     ▼
initialTableData(typeActiveTabKey)
     │
     ├─ 检查 allTypeData 是否为 null          → 空表 ✅
     ├─ 检查 allTypeData.uid === neUid        → 不匹配时空表 ✅
     ├─ 检查 allTypeData.data[type] 是否有数据 → 空表 ✅
     └─ 有数据 → getTableData → 展示 ✅

手动刷新流程(ConfigDetail)

复制代码
用户点击 reload
     │
     ▼
handleTypeChange(key, uid)
     │
     ├─ 取消上一次请求 (abortControllerRef.abort)
     ├─ 创建新 AbortController
     ├─ 发起 API 请求(支持 AbortSignal)
     ├─ 检查 controller.signal.aborted → 取消则不处理
     ├─ 成功 → getTableData → 更新表格
     └─ 失败 → catch → setAllDataSource([]) → finally → setLoading(false)

三个文件的改动详情

1. layout.tsx

新增变量:

  • fetchAbortRef --- useRef<AbortController>,用于取消上一次未完成的请求
  • fetchIdRef --- useRef<number>,递增计数器,确保只应用最新请求的响应

新增/修改函数:

  • getAllTypeData --- 添加 try-catch,写入 store 时携带 { uid, data } 结构

核心 useEffect(依赖 pathname):

复制代码
每次路由变化:
  1. 跳过公开路由(/login, /register, /forgot-password, /auth)
  2. 取消上一次请求
  3. 创建新 AbortController + 递增 fetchId
  4. 调用 getNeData(),成功后:
     a. 检查 controller.signal.aborted(取消则不处理)
     b. 排序重组 NE 列表 → setNeData()
     c. 核对 fetchId 一致性 → getAllTypeData(ne.uid)
  5. 清理函数 → 取消请求
  6. AbortError 静默忽略

2. store.ts

新增接口定义:

typescript 复制代码
interface AllTypeDataCache {
  uid: string;                // 该缓存所属的网元 ID
  data: Record<string, any>;  // 按类型分组的缓存数据
}

类型变更:

字段 之前 之后
allTypeData any `AllTypeDataCache
setAllTypeData 参数 (data: any) `(data: AllTypeDataCache

初始值变更:

typescript 复制代码
// 之前:空对象,语义上容易误认为 "缓存已就绪但为空"
allTypeData: {},
// 之后:null,明确表示 "尚未获取任何缓存"
allTypeData: null,

3. ConfigDetail.tsx

新增变量:

  • abortControllerRef --- useRef<AbortController>,每个 ConfigDetail 实例独立隔离
  • 卸载 cleanup useEffect --- 组件销毁时取消正在进行的请求

函数改动:

函数 改动
getCardPreConf 添加 try-catch,失败时初始化为 8 个空槽位
getStateData 添加 try-catch
getChassisData 添加 try-catch
handleTypeChange 重构:AbortController per-instance + try-catch-finally + loading 在 finally 中清除
initialTableData 添加 !allTypeDataallTypeData.uid !== neUid 双重防御

删除的死代码:

  • isLoadingRef --- 只写不读,已删除
  • if (!options) 分支 --- options 初始值始终为 truthy,无效守卫

关键设计决策

为什么要按 uid 隔离缓存

allTypeData 是全局 store 变量。如果仅存裸数据 { chassis: [...], allcard: [...] },则打开多 Tab(不同 NE)时,Tab-B 会读到 Tab-A 的缓存,显示错误数据。

改为 { uid: string, data: {...} } 后,Tab-B 读取时检查 allTypeData.uid !== neUid,发现不匹配直接返回空数据,保证不会显示错误数据。

为什么用 useRef 管理 AbortController

每个 ConfigDetail 实例需要独立的 AbortController,否则一个 Tab 的请求会取消另一个 Tab 的请求。useRef 保证每个组件实例有自己独立的引用。

为什么 try-catch-finally 是唯一正确模式

handleTypeChange 中,setLoading(false) 放在 finally 块中,保证无论成功、失败、还是异常,loading 状态都会被清除。这是 React 中处理异步 loading 的标准模式。


维护规则(供后续页面沿用)

规则 1:Layout 只负责刷新缓存

复制代码
Layout 只做两件事:
  → getNeData()        写入 neData(网元列表)
  → getAllTypeData()    写入 allTypeData(全局缓存池)
  
其他页面自己的数据在各自组件内管理

规则 2:缓存数据必须按业务 key 隔离

typescript 复制代码
// ✅ 正确:带 uid 标记
setAllTypeData({ uid: id, data: res.data });

// ❌ 错误:全局裸对象,多实例会互相覆盖
setAllTypeData(res.data);

规则 3:读缓存时校验归属

typescript 复制代码
// ✅ 正确的读缓存模式
if (!allTypeData || allTypeData.uid !== myUid) {
  setDataSource([]);
  return;
}
const data = allTypeData.data[type];

规则 4:所有 async 函数必须 try-catch,loading 必须 finally 清除

typescript 复制代码
const fetchData = async () => {
  setLoading(true);
  try {
    const res = await api();
    if (!res?.data) { setDataSource([]); return; }
    setDataSource(res.data);
  } catch (error) {
    console.warn('[Component] fetch failed:', error);
    setDataSource([]);
  } finally {
    setLoading(false);  // ❗保证一定被清除
  }
};

规则 5:请求去重 + 竞态防护

typescript 复制代码
const abortRef = useRef<AbortController | null>(null);

const doFetch = async () => {
  if (abortRef.current) abortRef.current.abort();
  const controller = new AbortController();
  abortRef.current = controller;
  
  const res = await fetch(url, { signal: controller.signal });
  if (controller.signal.aborted) return; // 过时响应不处理
  setData(res);
};

// 卸载时取消
useEffect(() => () => abortRef.current?.abort(), []);
相关推荐
恋猫de小郭1 小时前
Flutter 开发怎么做 Agent ?从工程实战详细给你解读下
android·前端·flutter
IT_陈寒1 小时前
React状态更新延迟搞晕我,原来是闭包在捣鬼
前端·人工智能·后端
晓得迷路了1 小时前
栗子前端技术周刊第 136 期 - pnpr、Flow 重构移植 Rust、Astryx...
前端·javascript·css
binbin_521 小时前
React Native 鸿蒙环境搭建完整实战:从 RN 工程到 RNOH 运行
深度学习·react native·react.js·harmonyos
柳杉1 小时前
别再手写大屏了!用这套「3D 地图 + 拖拽脚手架」示例源码,交付效率提升 80%
前端·three.js·数据可视化
乌夷2 小时前
Vue.js 3 的设计思路
前端·javascript·vue.js
丨我是张先生丨2 小时前
英语单词 Web Page
前端·css·css3
kgduu2 小时前
react-redux学习笔记
笔记·学习·react.js
随风一样自由2 小时前
【前端+Vitest测试框架】Vitest | Playwright | WebdriverIO | Preview
前端·前端框架·可用性测试