mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-13 07:43:43 +08:00
add 新增 KeepAliveTabs.tsx 支持 keep-alive 功能
This commit is contained in:
parent
912086075a
commit
693fd61e5d
73
src/components/layout/KeepAliveTabs.tsx
Normal file
73
src/components/layout/KeepAliveTabs.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { useLocation } from '@umijs/max';
|
||||
import { Activity } from 'react';
|
||||
// 与 @umijs/max 同一 react-router 实例 umi 构建时已做全局别名
|
||||
import { UNSAFE_LocationContext } from 'react-router-dom';
|
||||
import DynamicPage from '@/pages/dynamicPage';
|
||||
import HomePage from '@/pages/index';
|
||||
import ProfilePage from '@/pages/system/user/profile';
|
||||
import { keepAliveCache, useTagsViewStore, type CachedPageEntry } from '@/stores/tagsViewStore';
|
||||
|
||||
interface KeepAliveTabsProps {
|
||||
/** 刷新计数器 变化时重挂载当前激活的标签页 */
|
||||
refreshKey: number;
|
||||
}
|
||||
|
||||
function currentFullPath(pathname: string, search: string) {
|
||||
return `${pathname}${search || ''}`;
|
||||
}
|
||||
|
||||
/** 除动态路由外的两个静态布局路由 */
|
||||
function renderStaticPage(pathname: string) {
|
||||
if (pathname === '/index' || pathname === '/') return <HomePage />;
|
||||
if (pathname === '/user/profile') return <ProfilePage />;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 React 19 <Activity> 的标签页缓存(对标 Vue 版 keep-alive + cachedViews)
|
||||
* - 每个打开过的标签页对应一个 Activity 缓存实例 隐藏而非卸载 保留页面状态
|
||||
* - 关闭标签后不再渲染对应实例(等价于从 include 中剔除) 登出时 resetTags 清空缓存
|
||||
* - 通过 UNSAFE_LocationContext 冻结每个缓存页的路由上下文
|
||||
* 隐藏页不会被当前路由变化触发重渲染或重新请求
|
||||
*/
|
||||
export default function KeepAliveTabs({ refreshKey }: KeepAliveTabsProps) {
|
||||
const location = useLocation();
|
||||
const tags = useTagsViewStore(state => state.tags);
|
||||
|
||||
const activeKey = currentFullPath(location.pathname, location.search);
|
||||
|
||||
// refreshKey 变化说明用户点击了刷新 递增当前页版本号触发 Activity 重挂载
|
||||
keepAliveCache.noteRefresh(activeKey, refreshKey);
|
||||
|
||||
// 登记当前路由快照(新标签首次打开)
|
||||
keepAliveCache.register(activeKey, {
|
||||
pathname: location.pathname,
|
||||
search: location.search,
|
||||
hash: location.hash,
|
||||
state: location.state,
|
||||
key: location.key
|
||||
});
|
||||
|
||||
// 渲染集合 = 当前路由 + 已打开标签中登记过的页面(标签由 TagsView 在导航后登记 首帧需兜底渲染当前路由)
|
||||
const tagKeys = new Set(tags.map(tag => tag.key));
|
||||
tagKeys.add(activeKey);
|
||||
const entries: Array<[string, CachedPageEntry]> = [];
|
||||
for (const key of tagKeys) {
|
||||
const entry = keepAliveCache.get(key);
|
||||
if (entry) {
|
||||
entries.push([key, entry]);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{entries.map(([key, entry]) => (
|
||||
<Activity key={`${key}:${entry.version}`} mode={key === activeKey ? 'visible' : 'hidden'}>
|
||||
<UNSAFE_LocationContext.Provider value={{ location: entry.location }}>
|
||||
{renderStaticPage(entry.location.pathname) ?? <DynamicPage />}
|
||||
</UNSAFE_LocationContext.Provider>
|
||||
</Activity>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -14,6 +14,7 @@ import { isHandledRequestError } from '@/api/request';
|
||||
import defaultAvatar from '@/assets/images/profile.jpg';
|
||||
import appLogo from '@/assets/logo/logo.png';
|
||||
import ExternalLinkButton from '@/components/layout/ExternalLinkButton';
|
||||
import KeepAliveTabs from '@/components/layout/KeepAliveTabs';
|
||||
import LayoutSettings from '@/components/layout/LayoutSettings';
|
||||
import LocaleSelect from '@/components/layout/LocaleSelect';
|
||||
import MenuSearch from '@/components/layout/MenuSearch';
|
||||
@ -489,9 +490,16 @@ export default function BasicLayout() {
|
||||
/>
|
||||
)}
|
||||
{/* React 19.3 ViewTransition 页面切换过渡动画 不支持的浏览器自动降级为无动画 */}
|
||||
<ViewTransition>
|
||||
<Outlet key={`${location.pathname}${location.search}:${refreshKey}`} />
|
||||
</ViewTransition>
|
||||
{layoutSettings.tagsView ? (
|
||||
/* Activity 标签页缓存 切换标签隐藏而非卸载 保留页面状态(对标 Vue keep-alive) */
|
||||
<ViewTransition>
|
||||
<KeepAliveTabs refreshKey={refreshKey} />
|
||||
</ViewTransition>
|
||||
) : (
|
||||
<ViewTransition>
|
||||
<Outlet key={`${location.pathname}${location.search}:${refreshKey}`} />
|
||||
</ViewTransition>
|
||||
)}
|
||||
</ProLayout>
|
||||
<LayoutSettings
|
||||
open={settingsOpen}
|
||||
|
||||
@ -59,6 +59,42 @@ function normalizeTags(tags: TagViewItem[]) {
|
||||
return tags.some(item => item.key === homeTag.key) ? tags : [homeTag, ...tags];
|
||||
}
|
||||
|
||||
/** KeepAliveTabs 缓存页的路由快照(非响应式 模块级) */
|
||||
export interface CachedPageEntry {
|
||||
location: {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
state: unknown;
|
||||
key: string;
|
||||
};
|
||||
/** 重挂载版本号 刷新时变化 */
|
||||
version: number;
|
||||
}
|
||||
|
||||
const cachedPages = new Map<string, CachedPageEntry>();
|
||||
|
||||
/** 上一次渲染看到的刷新计数器 用于识别刷新动作 */
|
||||
let lastSeenRefreshKey: number | null = null;
|
||||
|
||||
export const keepAliveCache = {
|
||||
get: (key: string) => cachedPages.get(key),
|
||||
/** 登记新访问标签的路由快照 */
|
||||
register: (key: string, location: CachedPageEntry['location']) => {
|
||||
if (cachedPages.has(key)) return;
|
||||
cachedPages.set(key, { location, version: 0 });
|
||||
},
|
||||
/** refreshKey 变化时递增当前页版本号 触发 Activity 重挂载 */
|
||||
noteRefresh: (activeKey: string, refreshKey: number) => {
|
||||
if (lastSeenRefreshKey === refreshKey) return;
|
||||
lastSeenRefreshKey = refreshKey;
|
||||
const existing = cachedPages.get(activeKey);
|
||||
if (existing) {
|
||||
cachedPages.set(activeKey, { ...existing, version: existing.version + 1 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const useTagsViewStore = create<TagsViewState>((set, get) => ({
|
||||
tags: [homeTag, ...readPersistedTags().filter(item => item.key !== homeTag.key)],
|
||||
fullscreen: false,
|
||||
@ -82,6 +118,7 @@ export const useTagsViewStore = create<TagsViewState>((set, get) => ({
|
||||
},
|
||||
resetTags: () => {
|
||||
localStorage.removeItem(tagsStorageKey);
|
||||
cachedPages.clear();
|
||||
set({ tags: [homeTag], fullscreen: false });
|
||||
},
|
||||
setFullscreen: fullscreen =>
|
||||
|
||||
15
src/types/react-router-dom.d.ts
vendored
Normal file
15
src/types/react-router-dom.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
// umi 在构建时将 react-router-dom 别名到与 @umijs/max 相同的实例
|
||||
// 这里仅补充 tsc 需要的 UNSAFE_LocationContext 类型声明 供 KeepAliveTabs 冻结缓存页的路由上下文
|
||||
declare module 'react-router-dom' {
|
||||
import type { Context } from 'react';
|
||||
|
||||
export interface RouteLocation {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
state: unknown;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export const UNSAFE_LocationContext: Context<{ location: RouteLocation }>;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user