实现一个 React 版本的 Keep-Alive 组件,并支持 Tab 管理、缓存、关闭等功能

我将实现一个 React 版本的 Keep-Alive 组件,类似于 Vue 中的 keep-alive 功能,并支持 Tab 管理、缓存、关闭等功能。

实现方案

我们将创建以下几个核心组件和功能:

  1. KeepAlive - 缓存组件容器
  2. TabManager - 管理 Tab 的打开、关闭等操作
  3. useTab - 自定义 Hook 用于组件内访问 Tab 相关状态和方法
  4. ActivatedDeactivated 生命周期支持

1. KeepAlive 组件实现

ini 复制代码
import React, { useState, useEffect, useRef } from 'react';

// 缓存组件实例的容器
const KeepAlive = ({ children, isActive, onActivated, onDeactivated }) => {
  const mountedRef = useRef(false);
  const childRef = useRef();

  // 获取子组件的引用
  useEffect(() => {
    if (React.Children.count(children) === 1) {
      childRef.current = React.Children.only(children).ref;
    }
  }, [children]);

  // 当 isActive 变化时触发相应的生命周期
  useEffect(() => {
    if (isActive) {
      if (!mountedRef.current) {
        // 首次挂载
        mountedRef.current = true;
      } else {
        // 从非激活状态变为激活状态
        if (typeof onActivated === 'function') {
          onActivated();
        }
      }
    } else {
      // 从激活状态变为非激活状态
      if (typeof onDeactivated === 'function') {
        onDeactivated();
      }
    }
  }, [isActive, onActivated, onDeactivated]);

  return React.cloneElement(React.Children.only(children), {
    ref: (node) => {
      if (childRef.current) {
        if (typeof childRef.current === 'function') {
          childRef.current(node);
        } else {
          childRef.current.current = node;
        }
      }
    },
  });
};

export default KeepAlive;

2. TabManager 和 useTab 实现

javascript 复制代码
import React, { createContext, useContext, useState, useEffect } from 'react';

// 创建 Tab 上下文
const TabContext = createContext();

// TabManager 组件
export const TabManager = ({ children }) => {
  const [tabs, setTabs] = useState([]);
  const [activeTabId, setActiveTabId] = useState(null);

  // 添加新 Tab
  const addTab = (tab) => {
    setTabs((prevTabs) => {
      // 检查是否已存在相同路径的 Tab
      const existingTab = prevTabs.find((t) => t.path === tab.path);
      if (existingTab) {
        // 如果存在,激活它并返回不变
        setActiveTabId(existingTab.id);
        return prevTabs;
      }
      
      // 不存在则添加新 Tab
      const newTab = {
        id: Date.now().toString(),
        ...tab,
      };
      setActiveTabId(newTab.id);
      return [...prevTabs, newTab];
    });
  };

  // 关闭 Tab
  const closeTab = (tabId) => {
    setTabs((prevTabs) => {
      const newTabs = prevTabs.filter((tab) => tab.id !== tabId);
      
      // 如果关闭的是当前激活的 Tab
      if (tabId === activeTabId) {
        // 激活最后一个 Tab 或者第一个 Tab
        if (newTabs.length > 0) {
          setActiveTabId(newTabs[newTabs.length - 1].id);
        } else {
          setActiveTabId(null);
        }
      }
      
      return newTabs;
    });
  };

  // 关闭其他所有 Tab,保留当前 Tab
  const closeOtherTabs = () => {
    if (activeTabId) {
      setTabs((prevTabs) => prevTabs.filter((tab) => tab.id === activeTabId));
    }
  };

  // 关闭所有 Tab
  const closeAllTabs = () => {
    setTabs([]);
    setActiveTabId(null);
  };

  // 提供给子组件的值
  const value = {
    tabs,
    activeTabId,
    addTab,
    closeTab,
    closeOtherTabs,
    closeAllTabs,
  };

  return (
    <TabContext.Provider value={value}>
      {children}
    </TabContext.Provider>
  );
};

// useTab Hook
export const useTab = () => {
  const context = useContext(TabContext);
  if (!context) {
    throw new Error('useTab must be used within a TabManager');
  }
  return context;
};

3. Tab 组件实现

javascript 复制代码
import React from 'react';
import { useTab } from './TabManager';

const Tab = ({ tab }) => {
  const { closeTab } = useTab();

  return (
    <div className="tab">
      <span>{tab.title}</span>
      <button onClick={() => closeTab(tab.id)}>×</button>
    </div>
  );
};

export default Tab;

4. TabBar 组件实现

javascript 复制代码
import React from 'react';
import { useTab } from './TabManager';
import Tab from './Tab';

const TabBar = () => {
  const { tabs, activeTabId, closeTab, closeOtherTabs, closeAllTabs } = useTab();

  return (
    <div className="tab-bar">
      <div className="tab-actions">
        <button onClick={closeOtherTabs}>关闭其他</button>
        <button onClick={closeAllTabs}>关闭所有</button>
      </div>
      <div className="tabs">
        {tabs.map((tab) => (
          <Tab
            key={tab.id}
            tab={tab}
            isActive={tab.id === activeTabId}
          />
        ))}
      </div>
    </div>
  );
};

export default TabBar;

5. RouteTab 组件实现 (核心功能)

javascript 复制代码
import React, { useEffect } from 'react';
import { useLocation, useHistory } from 'react-router-dom';
import { useTab } from './TabManager';
import KeepAlive from './KeepAlive';

const RouteTab = ({ children }) => {
  const location = useLocation();
  const history = useHistory();
  const { tabs, activeTabId, addTab } = useTab();

  // 从路由信息中获取标题
  const getTabTitle = () => {
    // 这里可以根据路由配置获取标题
    // 简单实现:从 location.state 或 pathname 中提取
    return location.state?.title || location.pathname.split('/').pop() || '未命名';
  };

  // 当前路由对应的 Tab
  const currentTab = tabs.find((tab) => tab.path === location.pathname);

  // 添加新 Tab 或激活现有 Tab
  useEffect(() => {
    if (location.pathname) {
      addTab({
        path: location.pathname,
        title: getTabTitle(),
      });
    }
  }, [location.pathname, addTab]);

  // 如果没有匹配的 Tab,不渲染内容
  if (!currentTab) {
    return null;
  }

  // 判断当前 Tab 是否激活
  const isActive = currentTab.id === activeTabId;

  // 激活和停用回调
  const handleActivated = () => {
    console.log('Component activated:', currentTab.title);
    // 这里可以执行组件激活时的逻辑
  };

  const handleDeactivated = () => {
    console.log('Component deactivated:', currentTab.title);
    // 这里可以执行组件停用时的逻辑
  };

  return (
    <KeepAlive isActive={isActive} onActivated={handleActivated} onDeactivated={handleDeactivated}>
      {React.cloneElement(children, {
        // 可以传递一些额外的 props 给子组件
      })}
    </KeepAlive>
  );
};

export default RouteTab;

6. 完整使用示例

javascript 复制代码
import React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';
import { TabManager } from './TabManager';
import TabBar from './TabBar';
import RouteTab from './RouteTab';

// 示例页面组件
const Page1 = () => {
  console.log('Page1 rendered');
  return <div>Page 1 Content</div>;
};

const Page2 = () => {
  console.log('Page2 rendered');
  return <div>Page 2 Content</div>;
};

const Page3 = () => {
  console.log('Page3 rendered');
  return <div>Page 3 Content</div>;
};

const App = () => {
  return (
    <Router>
      <TabManager>
        <div className="app">
          <div className="nav">
            <Link to="/page1" state={{ title: '页面1' }}>页面1</Link>
            <Link to="/page2" state={{ title: '页面2' }}>页面2</Link>
            <Link to="/page3" state={{ title: '页面3' }}>页面3</Link>
          </div>
          
          <TabBar />
          
          <div className="content">
            <Switch>
              <Route path="/page1">
                <RouteTab>
                  <Page1 />
                </RouteTab>
              </Route>
              <Route path="/page2">
                <RouteTab>
                  <Page2 />
                </RouteTab>
              </Route>
              <Route path="/page3">
                <RouteTab>
                  <Page3 />
                </RouteTab>
              </Route>
            </Switch>
          </div>
        </div>
      </TabManager>
    </Router>
  );
};

export default App;
相关推荐
谢尔登5 分钟前
【React Native】ScrollView 和 FlatList 组件
javascript·react native·react.js
蓝婷儿22 分钟前
每天一个前端小知识 Day 27 - WebGL / WebGPU 数据可视化引擎设计与实践
前端·信息可视化·webgl
然我28 分钟前
面试官:如何判断元素是否出现过?我:三种哈希方法任你选
前端·javascript·算法
OpenTiny社区1 小时前
告别代码焦虑,单元测试让你代码自信力一路飙升!
前端·github
pe7er1 小时前
HTTPS:本地开发绕不开的设置指南
前端
晨枫阳1 小时前
前端VUE项目-day1
前端·javascript·vue.js
江山如画,佳人北望1 小时前
SLAM 前端
前端
患得患失9491 小时前
【前端】【Iconify图标库】【vben3】createIconifyIcon 实现图标组件的自动封装
前端
颜酱1 小时前
抽离ant-design后台的公共查询设置
前端·javascript·ant design
用户95251151401552 小时前
js最简单的解密分析
前端