Skip to content

Commit 4a8f25a

Browse files
committed
feat: keep alive
1 parent dcd8f35 commit 4a8f25a

File tree

13 files changed

+214
-7
lines changed

13 files changed

+214
-7
lines changed

README-en.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
[🔗Live Preview](https://template.react-antd-console.site) | [📒Documentation](https://doc.react-antd-console.site) | [中文](./README.md) | English
88

99
Now supports **React 19**!
10+
Now supports **KeepAlive** (experimental)!
1011

1112
<p align="center">
1213
<img width="100%" src="https://static.react-antd-console.site/template.png?g=1">
@@ -41,6 +42,7 @@ This project minimally encapsulates essential features like login, authenticatio
4142
- **💾 Data Management**: Layered (data and view) architecture design. The data management solution theoretically supports any UI rendering library/framework (including but not limited to React/Vue/Angular)
4243
- **🎨 Theme Customization**: Supports arbitrary color switching in dark/light modes
4344
- **🏷️ Multi-Tabs**: Draggable multi-tabs with persistence, right-click menus, etc.
45+
- **✨ Keep alive**: Supports page state caching, retaining the page state before switching when returning to the page.
4446
- **🎬 Elegant Animations**: Supports route transition animations, tab, menu, and button animations
4547
- **🧩 Other Features**: `Responsive design`, `internationalization`, `Mock`, `environment configuration`, `engineering standards`, etc.
4648

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
[🔗在线预览](https://template.react-antd-console.site) | [📒文档](https://doc.react-antd-console.site) | 中文 | [English](./README-en.md)
88

99
已支持**React 19**
10+
已支持**KeepAlive**(实验性)!
1011

1112
<p align="center">
1213
<img width="100%" src="https://static.react-antd-console.site/template.png?b=1">
@@ -39,6 +40,7 @@ react-antd-console 是一个后台管理系统的前端解决方案,封装了
3940
- **💾 数据管理**: `分层`(数据和视图)架构设计,数据管理方案理论上支持接入任意UI渲染库/框架(包括不限于React/Vue/Angular)
4041
- **🎨 颜色换肤**: 支持深/浅肤色模式下的任意颜色切换
4142
- **🏷️ 多标签页**: 可拖拽的多标签页,支持持久化、右键菜单等
43+
- **✨ 页面缓存**: 支持页面状态缓存,切换回页面后,保留切换前的页面状态
4244
- **🎬 优雅动画**: 支持路由切换动画,标签页、菜单、功能按钮动画等
4345
- **🧩 其他功能**: 如`响应式设计``国际化``Mock``环境配置``工程化规范`
4446

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default withMermaid({
4646
{ text: 'Icon', link: '/development/icon' },
4747
{ text: '国际化', link: '/development/i18n' },
4848
{ text: '搜索列表', link: '/development/search-list' },
49+
{ text: 'KeepAlive', link: '/development/keep-alive' },
4950
{ text: '常见问题', link: '/development/qa' },
5051
]
5152
}

docs/development/keep-alive.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# KeepAlive
2+
3+
`keepAlive` 功能,类似 `vue``KeepAlive` 功能,用于在组件间进行切换时,缓存被移除的组件实例
4+
5+
## 启用
6+
7+
使用 `KeepAlive` 能力需要安装 `react``react-dom``experimental` 版本:
8+
9+
```shell
10+
npm i react@experimental react-dom@experimental -S --legacy-peer-deps
11+
```
12+
13+
`ConsoleLayout` 组件中引入并使用 `KeepAliveOutlet` 代替 `react-router``Outlet`:
14+
15+
::: code-group
16+
17+
```tsx [src/layouts/ConsoleLayout/index.tsx]
18+
import { Outlet } from 'react-router'; // [!code --]
19+
import KeepAliveOutlet from '@/components/KeepAliveOutlet'; // [!code ++]
20+
import { motion } from 'framer-motion'; // [!code --]
21+
import { Animations } from './animations'; // [!code --]
22+
23+
return (
24+
<div>
25+
<div className="console-layout__right-side">
26+
{/* [!code --] */}
27+
<div className="console-layout__right-side-main-wrap">
28+
{refreshing ? null : ( // [!code --]
29+
<motion.div // [!code --]
30+
className={ClassName__ConsoleLayout_RightSideMain} // [!code --]
31+
key={location.pathname} // [!code --]
32+
variants={Animations['fadeIn']} // [!code --]
33+
initial="initial" // [!code --]
34+
animate="in" // [!code --]
35+
transition={{ type: 'tween', duration: 0.15, ease: 'easeIn' }} // [!code --]
36+
{/* [!code --] */}
37+
>
38+
{/* [!code --] */}
39+
<Outlet />
40+
</motion.div> // [!code --]
41+
{/* [!code --] */}
42+
)}
43+
{/* [!code --] */}
44+
</div>
45+
{/* [!code ++] */}
46+
<div className={`console-layout__right-side-main-wrap ${ClassName__ConsoleLayout_RightSideMain}`}>
47+
{/* [!code ++] */}
48+
{refreshing ? null : <KeepAliveOutlet />}
49+
{/* [!code ++] */}
50+
</div>
51+
</div>
52+
</div>
53+
);
54+
```
55+
56+
:::
57+
58+
## 查看效果
59+
60+
`/home/alive` 页的输入框输入一个数字,切换到其他页面,再切换回来,看看数字是否仍然在

docs/guide/what.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ react-antd-console 是一个后台管理系统的前端解决方案,封装了
2121
- **💾 数据管理**: `分层`(数据和视图)架构设计,数据管理方案理论上支持接入任意UI渲染库/框架(包括不限于React/Vue/Angular)
2222
- **🎨 颜色换肤**: 支持深/浅肤色模式下的任意颜色切换
2323
- **🏷️ 多标签页**: 可拖拽的多标签页,支持持久化、右键菜单等
24+
- **✨ 页面缓存**: 支持页面状态缓存,切换回页面后,保留切换前的页面状态
2425
- **🎬 优雅动画**: 支持路由切换动画,标签页、菜单、功能按钮动画等
2526
- **🧩 其他功能**: 如`响应式设计``国际化``Mock``环境配置``工程化规范`
2627

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@
4848
"i18next": "^25.1.2",
4949
"immer": "^10.1.1",
5050
"nprogress": "^0.2.0",
51-
"react": "^19.1.0",
51+
"react": "^0.0.0-experimental-8ce15b0f-20250522",
5252
"react-contexify": "^6.0.0",
53-
"react-dom": "^19.1.0",
53+
"react-dom": "^0.0.0-experimental-8ce15b0f-20250522",
5454
"react-i18next": "^15.5.1",
5555
"react-router": "^7.6.0",
5656
"react-router-toolset": "^0.0.9",
@@ -89,4 +89,4 @@
8989
"public"
9090
]
9191
}
92-
}
92+
}

src/assets/svg/kun.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/* eslint-disable @typescript-eslint/ban-ts-comment */
2+
import React, { useState, useEffect, useRef } from 'react';
3+
import { useOutlet, useLocation } from 'react-router';
4+
// import ActivityComponent from './Activity.js';
5+
6+
// @ts-ignore
7+
// const Activity = React.unstable_Activity || ActivityComponent;
8+
const Activity = React.unstable_Activity;
9+
interface ActivityItem {
10+
outlet: React.ReactElement | null;
11+
key: string;
12+
pathname: string;
13+
}
14+
15+
interface OutletProps {
16+
// The limitation number of outlets to keep alive.
17+
limit?: number;
18+
// When paths is configured, only the specified paths will be kept alive.
19+
paths?: string[];
20+
}
21+
22+
const OUTLET_LIMIT = 500;
23+
24+
export default function KeepAliveOutlet(props: OutletProps) {
25+
if (!Activity) {
26+
throw new Error('`<KeepAliveOutlet />` now requires react experimental version. Please install it first.');
27+
}
28+
const [outlets, setOutlets] = useState<ActivityItem[]>([]);
29+
const location = useLocation();
30+
const outlet = useOutlet();
31+
const outletLimit = props.limit || OUTLET_LIMIT;
32+
const keepAlivePaths = props.paths;
33+
34+
// Save the first outlet for SSR hydration.
35+
const outletRef = useRef<ActivityItem | null>({
36+
key: location.key,
37+
pathname: location.pathname,
38+
outlet,
39+
});
40+
useEffect(() => {
41+
// If outlets is empty, save the first outlet for SSR hydration,
42+
// and should not call setOutlets to avoid re-render.
43+
if (outlets.length !== 0 ||
44+
outletRef.current?.pathname !== location.pathname) {
45+
let currentOutlets = outletRef.current ? [outletRef.current] : outlets;
46+
// Check current path if exsist before filter, to avoid infinite setOutlets loop.
47+
const result = currentOutlets.some(o => o.pathname === location.pathname);
48+
if (keepAlivePaths && keepAlivePaths.length > 0) {
49+
currentOutlets = currentOutlets.filter(o => keepAlivePaths.includes(o.pathname));
50+
}
51+
if (!result) {
52+
setOutlets([
53+
...currentOutlets,
54+
{
55+
key: location.key,
56+
pathname: location.pathname,
57+
outlet,
58+
},
59+
].slice(-outletLimit));
60+
outletRef.current = null;
61+
}
62+
}
63+
}, [location.pathname, location.key, outlet, outlets, outletLimit, keepAlivePaths]);
64+
65+
// Render initail outlet for SSR hydration.
66+
const renderOutlets = outlets.length === 0 ? [outletRef.current] : outlets;
67+
68+
return (
69+
<>
70+
{
71+
renderOutlets.map((o) => {
72+
return (
73+
<Activity key={o?.key} mode={location.pathname === o?.pathname ? 'visible' : 'hidden'}>
74+
{o?.outlet}
75+
</Activity>
76+
);
77+
})
78+
}
79+
</>
80+
);
81+
}

src/layouts/ConsoleLayout/index.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FC } from 'react';
2-
import { Outlet } from 'react-router';
2+
// import { Outlet } from 'react-router';
3+
import KeepAliveOutlet from '@/components/KeepAliveOutlet';
34
import SideMenu from '@/layouts/SideMenu';
45
import withAuth from '@/components/business/withAuth';
56
import Header from '@/layouts/Header';
@@ -9,8 +10,8 @@ import { baseModel } from '@/models/base';
910
import { ClassName__ConsoleLayout_RightSideMain } from './consts';
1011
import Provider from './store/Provider';
1112
import Tabs from '@/layouts/Tabs';
12-
import { motion } from 'framer-motion';
13-
import { Animations } from './animations';
13+
// import { motion } from 'framer-motion';
14+
// import { Animations } from './animations';
1415
import Collapse from '../Collapse';
1516
import classNames from 'classnames';
1617
import { isMobile } from '@/utils/browser';
@@ -40,7 +41,7 @@ const ConsoleLayout: FC = () => {
4041
<Collapse />
4142
<Tabs />
4243
</div>
43-
<div className="console-layout__right-side-main-wrap">
44+
{/* <div className="console-layout__right-side-main-wrap">
4445
{refreshing ? null : (
4546
<motion.div
4647
className={ClassName__ConsoleLayout_RightSideMain}
@@ -53,6 +54,9 @@ const ConsoleLayout: FC = () => {
5354
<Outlet />
5455
</motion.div>
5556
)}
57+
</div> */}
58+
<div className={`console-layout__right-side-main-wrap ${ClassName__ConsoleLayout_RightSideMain}`}>
59+
{refreshing ? null : <KeepAliveOutlet />}
5660
</div>
5761
<Footer />
5862
</div>

src/models/withAuth/permissions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function formatPermissions(permissions: string[]) {
77
return {
88
home: set.has('home'),
99
homeIndex: set.has('home:index'),
10+
homeAlive: set.has('home:alive'),
1011
homeGrid: set.has('home:grid'),
1112
profile: set.has('profile'),
1213

0 commit comments

Comments
 (0)