一个基于 React Router v6 的“最小可复用” KeepAlive Tabs 方案:路由驱动多页签、拖拽排序、右键菜单、页面缓存与刷新恢复。仓库本身是可直接运行与二次改造的 Demo 工程。
- 在线预览(GitHub Pages):https://aiyoudiao.github.io/keepalive-tabs-kit/
- Storybook:同上(Pages 里就是 Storybook 静态站点)
- 路由驱动 Tabs:访问路由自动生成页签
- KeepAlive:Tab 切换不卸载页面(组件 state 保留)
- 拖拽排序:支持调整 Tab 顺序,并持久化到
sessionStorage - 右键菜单:重新加载、关闭当前、关闭左侧/右侧、关闭其它
- 刷新恢复:刷新后从
sessionStorage恢复 Tab 列表 - 错误兜底:ErrorBoundary 捕获渲染期异常并给出可恢复 UI
- 依赖极简:React / React Router / Antd / @dnd-kit / Vite / TypeScript
- Node.js 18+(推荐 20)
- pnpm 8+(建议用 corepack 固定 pnpm 版本)
pnpm install
pnpm dev打开:
pnpm build这套实现更偏“可复制粘贴的 Kit”,推荐直接把实现文件带走:
- 拷贝目录:
src/components/KeepAliveTabs/*
- 拷贝样式(或按你项目的布局体系改造):
src/index.css中与.app-shell/.tabs-bar/.tabs-content相关部分
- 在你的路由入口中,把根路由的
element换成KeepAliveLayout,并传入routeConfig(用于 Tab 标题、图标与 keepAlive 开关)
示例(可直接参考现成实现):routes/index.tsx
import { createBrowserRouter } from 'react-router-dom';
import { KeepAliveLayout, RouteConfig } from '@/components/KeepAliveTabs';
const routeConfig: RouteConfig = {
'/': { name: '首页' },
'/about': { name: '关于' },
'/counter/:id': { name: 'Counter' },
'/404': { name: '404', keepAlive: false },
};
export const router = createBrowserRouter([
{
path: '/',
element: <KeepAliveLayout routeConfig={routeConfig} />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
{ path: 'counter/:id', element: <Counter /> },
{ path: '*', element: <NotFound /> },
],
},
]);如果你想快速验证一份“可复制的最小用法”,也可以看 Storybook 的 MemoryRouter 版本:KeepAliveTabs.stories.tsx
从 components/KeepAliveTabs/index.ts 导出:
type Props = {
routeConfig: RouteConfig;
};routeConfig:Record<string, RouteInfo>- key 支持静态路由(
/about)与动态路由 pattern(/counter/:id) - 内部使用
matchPath(pattern, pathname)匹配动态路由
- key 支持静态路由(
type RouteInfo = {
name: string;
icon?: React.ReactNode;
keepAlive?: boolean;
};name:Tab 标题icon:Tab 图标(可选)keepAlive:默认开启;显式设为false时,该路由不走缓存(直接渲染 outlet)
- 默认使用
sessionStorage['__keepalive_tabs_list__']持久化 Tab 路径(统一转小写)
- 关键文件:
- KeepAlive 核心:KeepAliveLayout.tsx
- Tabs UI(拖拽 + 右键):TabsBar.tsx
- 类型:types.ts
- 重要约束:
- 不要将
useOutlet()返回值放入 React state(容易触发 “Maximum update depth exceeded” 的循环更新)。当前实现将缓存内容写入 ref,并在渲染时按 activePath 选择展示。
- 不要将
pnpm dev # 本地开发
pnpm build # 构建(Vite + tsc)
pnpm preview # 本地预览构建产物
pnpm storybook # 启动 Storybook
pnpm build-storybook # 构建 Storybook(输出 storybook-static/,用于 Pages)
pnpm test # 运行测试(vitest)
pnpm typecheck # 类型检查useOutlet() 的返回值在每次渲染时可能是新引用。如果 effect 依赖 outlet 并 setState,会导致无限更新循环。当前实现把缓存内容写入 ref,避免这个问题。
使用 sessionStorage['__keepalive_tabs_list__'] 保存 Tab 的路径(pathname 小写),刷新后用它恢复 Tab 列表。
欢迎 PR:
MIT License,见 LICENSE。


