React 多页签方案 ~~~ 基于第三方库React Activation实现


前言

在一些项目上会有需要多页签的需求,vue可以通过keepalive去实现,react暂时没有这方面的支持,所以可以考虑使用第三方库来实现


一、React Activation

React Activation:在react中实现vue中的keepalive功能,不过本人技术差,没去深究原理,感兴趣的可以进入github中了解

二、使用步骤

1.安装

yarn add react-activation
或者
npm install react-activation

2.基本实现

PageTabsLayout.js
代码实现方式在尽量不影响原来的代码的情况下做出扩展
完整代码如下(示例):


import { DownOutlined } from '@ant-design/icons';
import { Button, Dropdown, Menu, Tabs } from 'antd';
import classnames from 'classnames/bind';
import { useEffect, useState } from 'react';
import KeepAlive, { AliveScope, useAliveController } from 'react-activation';
import { connect, history } from 'umi';

const { TabPane } = Tabs;
const cx = classnames.bind();

const PageTabsLayout = (props) => {
  const {
    location: { pathname, search, query }, route, children, dispatch,
    tabInfo: { tabList, activeKey, allowedData },
  } = props;
  const { dropScope, refresh } = useAliveController(); // 清除缓存函数
  const [first, setFirst] = useState(true); // 是否首次加载

  /**
   * 修改页签数据
   */
  const changeTabList = (data) => dispatch({ type: 'tab/_setTabList', data });
  /**
   * 修改选中页签key
   */
  const changeActiveKey = (key) => dispatch({ type: 'tab/_setActiveKey', key });
  /**
   * 修改可激活页签的路由数据
   */
  const changeAlloweData = (data) => dispatch({ type: 'tab/_setAllowedData', data });

  /**
   * 根据路由名称获取该路由是否为可激活页签
   */
  const getAllowed = (pathname) => allowedData.find(item => item.path == pathname);

  /**
   * 根据key获取tab数据
   */
  const getTab = (key) => tabList.find(item => item.key == key);

  /**
   * 根据key清除页签的页面缓存
   * @param {String} value keys的字符串,传递多个可用逗号分割
   */
  const dropScopePage = (value) => value.split(',').forEach(item => dropScope(item));

  /**
   * 根据key获取tab在数组中的索引
   */
  const getTabIndex = (key) => {
    const { tabInfo: { tabList } } = props;
    return tabList.findIndex(item => item.key === key);
  }

  /**
   * 根据数组中的索引修改或删除对应的tab
   * @param {*} index 数组索引
   * @param {*} newData 为空时即为删除,否则为修改
   */
   const changeTabByIndex = (index, newData = undefined) => {
    const { tabInfo: { tabList } } = props;
    const newTabList = [...tabList];
    if (!!newData) {
      // 有新数据,修改
      newTabList.splice(index, 1, newData);
    } else {
      // 没有新数据,删除
      newTabList.splice(index, 1);
    }
    changeTabList(newTabList);
  }

  /**
   * 获取当前链接拼接的key名,如果有附带参数,key = key + search
   */
  const getTabKeyByRoute = () => {
    const isOnlyPage = getIsOnlyPage(pathname);
    if (isOnlyPage) {
      // 标识该页面只能打开一次页签,不允许多开,所以使用路由名称做为key值
      return pathname;
    }
    let searchKey = search;
    if (searchKey) {
      // 在编辑页面有传入ID后,浏览器的刷新会导致seach属性的 '?' 丢失,所以加上判断,没有则加上
      if (searchKey.indexOf('?') != 0) {
        searchKey = `?${searchKey}`;
      }
      return pathname + searchKey;
    }
    return pathname;
  };

  /**
   * 获取当前路由是否只允许打开一个页签
   */
  const getIsOnlyPage = (pathname) => !!getAllowed(pathname)?.meta?.isOnlyPage;

  /**
   *  设置当前页签的标题
   */
   const setTabTitle = (title) => {
    const { tabInfo: { tabList } } = props;
    const newTabList = [...tabList];
    const tabIndex = getTabIndex(activeKey);
    if (tabIndex > -1) {
      const newTab = { ...newTabList[tabIndex], name: title };
      changeTabByIndex(tabIndex, newTab);
    }
  }

  /**
   * @param {*} key 页签key
   */
   const onClosePage = (key) => {
    if (!!key) return close(key);
    const routeKey = getTabKeyByRoute();
    close(routeKey);
  }

  const routeKey = getTabKeyByRoute(); // 当前路由跟参数拼接出来的key

  // 首次运行 - 只加载一次
  useEffect(() => {
    const { tabList: newTabList, allowedData: newAllowedData } = handleRouteData(route.routes);
    let nowRouteKey = routeKey;
    const tab = newTabList.find(item => item.key == nowRouteKey);
    const allowed = newAllowedData.find(item => item.path == pathname);
    if (!tab && !!allowed) {
      // 只允许打开一个页签
      if (allowed.meta.isOnlyPage) {
        nowRouteKey = pathname;
      }
      // 首次加载 且是 可激活页签
      newTabList.push({
        ...allowed,
        key: nowRouteKey,
        query: query,
      });
    }
    changeTabList(newTabList); // 已激活的页签
    changeAlloweData(newAllowedData); // 可以激活页签的路由
    changeActiveKey(nowRouteKey); // 激活的页签key
    setFirst(false); // 设置是否首次加载为false
  }, []);

  const allowed = getAllowed(pathname); // 需要缓存路由的数据
  const routeWhen = !!allowed; // 当前路由是否需要缓存、激活页签

  // 点击页签变化
  const changeTabByKey = (key) => {
    if (key == routeKey) return;
    const tab = getTab(key);
    changeActiveKey(key);
    history.replace({
      pathname: tab.path,
      query: tab.query,
    });
  }

  // 页签关闭事件 - 关闭open
  const removeTabey = (activeKey, action) => {
    if (action == 'remove') {
      close(activeKey);
    }
  };

  // 关闭页签
  const close = (key) => {
    const { tabInfo: { tabList } } = props;
    const newTabList = [...tabList];
    const tabIndex = getTabIndex(key);
    if (tabIndex > -1) {
      changeTabByIndex(tabIndex);
      const isActive = props?.tabInfo?.activeKey === key; // 是否被激活的
      if (isActive) { // 被删除的是当前激活的页签
        const beforeIndex = tabIndex - 1 < 0 ? 0 : tabIndex - 1; // 取的被删页签的前一条数据索引
        const tab = newTabList[beforeIndex]; // 获取数据
        if (tab) changeTabByKey(tab.key); // 改变激活key
      }
      // removePage(key); // 删除该页签缓存的事件函数
      dropScopePage(key); // 清除缓存
    }
  }

  // 菜单点击
  const menuItemClick = (key) => {
    if (key === 'closeOther') {
      // 清除其它选项卡
      closeOther(routeKey);
    }
    if (key === 'closeAll') {
      // 清除全部选项卡
      closeAll();
    }
  };

  // 删除除了key以外所有可关闭的页签
  const closeOther = (key) => {
    const { tabInfo: { tabList } } = props;
    const arr = [...tabList];
    const newTabList = arr.filter(item => item.key === key || item.meta.closable === false);
    const delKeys = arr.filter(item => item.key !== key && item.meta.closable !== false).map(item => item.key).join();
    // removePage(delKeys); // 删除该页签缓存的事件函数
    dropScopePage(delKeys); // 清除页面缓存
    changeTabList(newTabList); // 改变数据
  };

  // 删除所有可关闭的页签
  const closeAll = () => {
    const { tabInfo: { tabList } } = props;
    const arr = [...tabList];
    const newTabList = arr.filter(item => item.meta.closable === false);
    const delKeys = arr.filter(item => item.meta.closable !== false).map(item => item.key).join();
    if (newTabList.length > 0) {
      const tab = newTabList[newTabList.length - 1];
      changeTabByKey(tab.key);
    } else {
      history.replace({
        pathname: '/',
      });
    }
    // removePage(delKeys); // 删除该页签缓存的事件函数
    dropScopePage(delKeys); // 清除页面缓存
    changeTabList(newTabList); // 改变数据
  }

  // 额外内容渲染
  const extraRender = () => {
    const items = [
      { label: '清除其它选项卡', key: 'closeOther' },
      { label: '清除全部选项卡', key: 'closeAll' },
    ];
    const menu = <Menu items={items} onClick={(e) => menuItemClick(e.key)} />;
    return (
      <div className='tabs_extra'>
        <Dropdown overlay={menu} overlayStyle={{ minWidth: 150 }}>
          <Button className="tabs_extra_btn">
            关闭操作 <DownOutlined />
          </Button>
        </Dropdown>
      </div>
    );
  };

  // 监听路由变化,使用routeKey的原因是可能存在同个详情页,但是参数不同
  useEffect(() => {
    if (first) return;
    const { tabInfo: { tabList, activeKey } } = props;
    const isOnlyPage = getIsOnlyPage(pathname);
    const tab = getTab(routeKey);
    const newTabList = [...tabList];
    if (activeKey !== routeKey) {
      if (tab && isOnlyPage && routeWhen) { // 页签存有该数据,但标记只能打开一个页签,替换掉旧的页签
        refresh(routeKey); // 刷新该路由缓存
        const tabIndex = getTabIndex(routeKey);
        newTabList.splice(tabIndex, 1, { ...tab, query });
        history.replace({
          pathname: tab.path,
          query,
        });
      } else if (tab) { // 页签存有该数据
        history.replace({
          pathname: tab.path,
          query: tab.query,
        });
      } else {// 页签没有该数据
        if (routeWhen) { // 是可激活页签
          newTabList.push({
            ...allowed,
            key: routeKey,
            query: query,
          });
        }
      }
      changeTabList(newTabList);
      changeActiveKey(routeKey);
    }
  }, [routeKey]);

  /**
   * 获取tab组件需要的数据渲染标签栏
   * @return {Array} 返回结果
   */
  const getTabItems = () => {
    return tabList.map(item => {
      const newItem = { ...item, label: item.name, closable: item.meta?.closable };
      return newItem
    });
  }

  return (
    <div className={cx('page-tabs-layout')}>
      <div className={cx('page-tabs-layout-body')}>
        <Tabs
          className={`page-tabs-layout-body-tabs`}
          type="editable-card"
          hideAdd
          activeKey={activeKey}
          onEdit={removeTabey}
          onChange={changeTabByKey}
          tabBarExtraContent={extraRender()}
          tabBarGutter={0}
          items={getTabItems()}
        />
      </div>
      <div className={cx('page-tabs-layout-content')} >
        <AliveScope>
          <KeepAlive when={routeWhen} name={routeKey} id={routeKey} saveScrollPosition="screen" >
            {children}
          </KeepAlive>
        </AliveScope>
      </div>
    </div>
  );
}

export default connect(({ tab }) => ({
  tabInfo: tab,
}))(PageTabsLayout);


// 路由处理,将数据处理成列表,提供给tab页签
const handleRouteData = (children) => {
  const [tabList, allowedData] = [[], []];
  const formatData = (data) => {
    data.forEach((item) => {
      const meta = item.meta;
      if (meta) {
        // 展示在最左方,且不可关闭
        if (meta.keepAlive && meta.closable == false) {
          tabList.push({
            ...item,
            key: item.path,
          });
        }
        // 该路由标记需要页签
        if (meta.keepAlive) allowedData.push(item);
      }
      // 该路由存在子路由
      if (item.routes) formatData(item.routes);
    });
  };
  formatData(children);
  return { tabList, allowedData };
};

tab.js 下方是用于存储数据的,这个没什么太大改造

export default {
  namespace: 'tab',
  state: {
    activeKey: '', // 当前页签的激活key
    tabList: [], // 已激活的页签数据
    allowedData: [], // 记录可被激活为页签的路由
  },
  reducers: {
    _setActiveKey (state, { key }) {
      return { ...state, activeKey: key };
    },
    _setTabList (state, { data }) {
      return { ...state, tabList: data };
    },
    _setAllowedData (state, { data }) {
      return { ...state, allowedData: data };
    },
  },
};

route.config.js为了更好的辨认哪些组件需要页签,哪些直接打开,都通过路由定义中更改

      {
        // 首页
        path: '/home',
        component: '@/pages/Home',
        name: '首页',
        meta: {
          keepAlive: true, // 需要缓存页签
          closable: false, // 页签不允许关闭
          isOnlyPage: true, // 这个定义为了某些模块不想多开,只能让用户开启一个的情况下使用,加入后,如果URL传入的参数不同,会清空该模块的缓存
        },
      },

三、截图展示

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


总结

以上是我对于react多页签方案做出一些完善,有想了解更多的可以评论里提问哦,我会的一定会给你解答

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值