# 路由页面缓存使用说明
本项目使用 [`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` 为准。