# 路由页面缓存使用说明 本项目使用 [`react-activation`](https://github.com/CJY0208/react-activation) 实现 Layout 内页面的 KeepAlive 缓存。切走再回来时,页面组件不会卸载,滚动位置和本地 state 会保留。 --- ## 1. 整体结构 ``` main.tsx └─ AliveScope ← 全局必须,否则 KeepAlive 无效 └─ RouterProvider └─ LayoutRoot ← 带侧栏的业务页统一走这里 ├─ RouteCacheController ← LRU 淘汰(在 KeepAlive 树外) └─ Layout └─ CachedLayoutOutlet ← 按规则决定是否包 KeepAlive └─ 各页面组件 ``` | 文件 | 职责 | |------|------| | `src/main.tsx` | 挂载 `AliveScope` | | `src/router/reactActivationSetup.ts` | React 18 兼容(关 `autoFreeze`、修 Context) | | `src/router/LayoutRoot.tsx` | Layout 只挂一次 + 组装缓存控制器 | | `src/router/CachedLayoutOutlet.tsx` | 判断是否缓存、包裹 `KeepAlive` | | `src/router/RouteCacheController.tsx` | 超出上限时 LRU 淘汰 | | `src/router/routeCache.ts` | **缓存规则配置中心(改这里最多)** | | `src/router/index.tsx` | 路由表:哪些页在 `LayoutRoot` 下 | --- ## 2. 默认哪些路由会缓存 ### 会缓存 挂在 `LayoutRoot` 的 `children` 里、且 `isLayoutRouteCacheable(pathname)` 返回 `true` 的路由。 当前默认缓存: | 路径 | 页面 | |------|------| | `/explore` | 探索 | | `/characters` | 角色 | | `/characters-create` | 角色创建 | | `/subscribe` | 订阅 | | `/profile` | 个人中心 | | `/legal` | 法律条款 | | `/content/page` | 内容页 | ### 不会缓存 1. **不在 `LayoutRoot` 下的路由**(根本不会经过 `CachedLayoutOutlet`) 例如:`/index`、`/404` 2. **在 `isLayoutRouteCacheable` 里被排除的路由** 当前排除:`/create` 及所有 `/create/*` 子路由 --- ## 3. 如何让新页面加入缓存 需要同时满足两点。 ### 步骤 A:路由挂到 `LayoutRoot` 下 在 `src/router/index.tsx` 的 `LayoutRoot` → `children` 中增加路由: ```tsx { element: , children: [ // ... { path: "my-new-page", // 对应 /my-new-page element: , }, ], }, ``` 只要页面走 `LayoutRoot`,且未被 `isLayoutRouteCacheable` 排除,就会**自动**进入 KeepAlive,无需再改 `CachedLayoutOutlet`。 ### 步骤 B:确认未被排除规则拦截 打开 `src/router/routeCache.ts`,检查 `isLayoutRouteCacheable` 是否对该 pathname 返回 `true`。 --- ## 4. 如何让页面不加入缓存 有三种方式,按场景选择。 ### 方式一:不挂在 `LayoutRoot` 下(整页无侧栏) 适合落地页、404 等本来就不要 Layout 的页面。 ```tsx // 与 LayoutRoot 平级,直接渲染 { path: ROUTES.INDEX, element: ( }> ), }, ``` ### 方式二:在 `isLayoutRouteCacheable` 中排除(推荐) 适合:有 Layout 侧栏,但希望每次进入都重新加载(如 `/create`)。 编辑 `src/router/routeCache.ts`: ```ts /** 不参与 KeepAlive 的路由前缀 / 精确路径 */ const LAYOUT_ROUTE_CACHE_EXCLUDED_EXACT = new Set([ ROUTES.CREATE, ROUTES.CHARACTERS_CREATE, // 示例:角色创建也不缓存 ]); const LAYOUT_ROUTE_CACHE_EXCLUDED_PREFIXES = [ `${ROUTES.CREATE}/`, // /create/* 全部不缓存 ] as const; export function isLayoutRouteCacheable(pathname: string): boolean { if (LAYOUT_ROUTE_CACHE_EXCLUDED_EXACT.has(pathname)) { return false; } if (LAYOUT_ROUTE_CACHE_EXCLUDED_PREFIXES.some((prefix) => pathname.startsWith(prefix))) { return false; } return true; } ``` 当前项目用的是内联判断,逻辑等价于: ```ts export function isLayoutRouteCacheable(pathname: string): boolean { return pathname !== ROUTES.CREATE && !pathname.startsWith(`${ROUTES.CREATE}/`); } ``` ### 方式三:把路由移出 `LayoutRoot` 有侧栏需求但不想缓存时,更推荐方式二;移出 `LayoutRoot` 会导致侧栏也消失,一般不要用。 --- ## 5. 缓存 key 与「同一页多份缓存」 默认 `getLayoutRouteCacheKey` 使用完整 `pathname`: ```ts export function getLayoutRouteCacheKey(pathname: string): string { return pathname; } ``` 含义: - `/explore` 与 `/characters` 各一份缓存 - 带 query 的路径(如 `/profile?tab=1`)pathname 不变时,仍共用同一份缓存(search 不参与 key) 若多个 pathname 要共用一份缓存(例如某模块下多个 Tab 共用一个壳),可在 `getLayoutRouteCacheKey` 里做归一化: ```ts export function getLayoutRouteCacheKey(pathname: string): boolean { if (pathname.startsWith("/some-module/")) { return "/some-module"; } return pathname; } ``` 注意:共用 key 时,子路由切换由 Router `Outlet` 驱动,子页面仍可能随 Outlet 卸载。仅在你明确需要「整壳保留、内部自行切换」时使用。 --- ## 6. 缓存数量上限(LRU) 在 `src/router/routeCache.ts`: ```ts export const LAYOUT_ROUTE_CACHE_MAX = 8; ``` 超出后,`RouteCacheController` 会按最近访问顺序淘汰最久未用的可缓存页面。当前页不会被淘汰。 只统计 `isLayoutRouteCacheable(key) === true` 的节点,被排除的路由(如 `/create`)不会占名额。 --- ## 7. 页面内:再次显示时刷新数据 缓存只保留组件实例,不会自动重拉接口。若「切回来要更新数据」,在页面里用: ```tsx import { useActivate } from "react-activation"; function ExplorePage() { useActivate(() => { // 从缓存恢复、重新可见时执行 refetchList(); }); // ... } ``` 隐藏/切走时可配合: ```tsx import { useUnactivate } from "react-activation"; useUnactivate(() => { // 页面被缓存隐藏时(可选) }); ``` --- ## 8. 手动清除缓存 在任意 `AliveScope` 内的组件中(建议 Layout 或全局逻辑,不要放在 KeepAlive 子树深处): ```tsx import { useAliveController } from "react-activation"; function SomeComponent() { const { drop, refresh, clear, getCachingNodes } = useAliveController(); // 删除指定页的缓存(name / cacheKey 与 pathname 一致,除非改过 getLayoutRouteCacheKey) void drop("/explore"); // 刷新:卸载后下次进入重新挂载 void refresh("/explore"); // 清空全部缓存 void clear(); // 查看当前缓存列表 console.log(getCachingNodes()); } ``` 常见场景: | 场景 | 建议 | |------|------| | 用户退出登录 | `clear()` | | 某列表页强制刷新 | `refresh("/explore")` | | 发布内容后探索页要最新 | `useActivate` 里 refetch,或 `refresh` | --- ## 9. 配置检查清单 新增或调整缓存行为时,可按此核对: - [ ] 路由是否在 `LayoutRoot.children` 内? - [ ] `isLayoutRouteCacheable` 对该 pathname 是否为 `true`? - [ ] `getLayoutRouteCacheKey` 是否需要自定义合并 key? - [ ] 重页面是否应排除缓存(内存占用)? - [ ] 切回时是否需要 `useActivate` 拉新数据? - [ ] `LAYOUT_ROUTE_CACHE_MAX` 是否需要调整? --- ## 10. 注意事项 1. **`AliveScope` 不能删** 在 `src/main.tsx`,且须包住 `RouterProvider`。 2. **不要在 KeepAlive 内部再嵌套 KeepAlive** 也不要在 KeepAlive 子树里使用 `useAliveController` 做 LRU,容易报错或死循环。淘汰逻辑只在 `RouteCacheController` 一处维护。 3. **不要开启 React StrictMode + 嵌套 Alive** 当前 `main.tsx` 已关闭 StrictMode,与 react-activation 兼容有关。 4. **Layout 侧栏不会因切页卸载** `LayoutRoot` 只挂载一次;缓存的是 `CachedLayoutOutlet` 里的页面区域,不是整个 Layout。 5. **query 变化不拆缓存** 若业务依赖 `?mcRefresh=xxx` 等同 pathname 刷新,请在页面内监听 `useLocation().search`,或使用 `useActivate` / `refresh`,不要指望 remount。 --- ## 11. 快速示例汇总 ### 新增可缓存页面 `/settings` ```tsx // router/index.tsx → LayoutRoot.children { path: "settings", element: } ``` 无需改 `routeCache.ts`(默认即可缓存)。 ### 排除 `/settings` 不缓存 ```tsx // routeCache.ts export function isLayoutRouteCacheable(pathname: string): boolean { if (pathname === "/settings") return false; return pathname !== ROUTES.CREATE && !pathname.startsWith(`${ROUTES.CREATE}/`); } ``` ### 调整最多缓存 5 个页面 ```tsx // routeCache.ts export const LAYOUT_ROUTE_CACHE_MAX = 5; ``` --- ## 12. 相关依赖 - `react-activation`:KeepAlive 实现 - `react-router-dom` v6:`createBrowserRouter` + `LayoutRoot` 嵌套路由 文档与代码不一致时,以 `src/router/routeCache.ts` 及 `src/router/index.tsx` 为准。