Skip to content

aiyoudiao/keepalive-tabs-kit

KeepAlive Tabs Kit

Deploy Storybook to GitHub Pages License

一个基于 React Router v6 的“最小可复用” KeepAlive Tabs 方案:路由驱动多页签、拖拽排序、右键菜单、页面缓存与刷新恢复。仓库本身是可直接运行与二次改造的 Demo 工程。

演示

KeepAlive Tabs Demo 1

KeepAlive Tabs Demo 2

KeepAlive Tabs Demo 2

目录

特性

  • 路由驱动 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”,推荐直接把实现文件带走:

  1. 拷贝目录:
    • src/components/KeepAliveTabs/*
  2. 拷贝样式(或按你项目的布局体系改造):
    • src/index.css 中与 .app-shell/.tabs-bar/.tabs-content 相关部分
  3. 在你的路由入口中,把根路由的 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

API 说明

KeepAliveLayout

从 components/KeepAliveTabs/index.ts 导出:

type Props = {
  routeConfig: RouteConfig;
};
  • routeConfigRecord<string, RouteInfo>
    • key 支持静态路由(/about)与动态路由 pattern(/counter/:id
    • 内部使用 matchPath(pattern, pathname) 匹配动态路由

RouteInfo

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        # 类型检查

常见问题

为什么不用把 outlet 放进 state?

useOutlet() 的返回值在每次渲染时可能是新引用。如果 effect 依赖 outletsetState,会导致无限更新循环。当前实现把缓存内容写入 ref,避免这个问题。

刷新恢复存在哪里?

使用 sessionStorage['__keepalive_tabs_list__'] 保存 Tab 的路径(pathname 小写),刷新后用它恢复 Tab 列表。

贡献

欢迎 PR:

安全

许可证

MIT License,见 LICENSE

About

React Router v6 的最小 KeepAlive Tabs 方案:用“多页签 + 可拖拽 + 页面缓存 + 刷新恢复”验证主流程,一个可直接复用到其它项目的 Demo 工程。

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages