文章目录
- 前言
- 一、React Activation
- 二、使用步骤
- 1.安装
- 2.基本实现
- 三、截图展示
- 总结
前言
在一些项目上会有需要多页签的需求,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多页签方案做出一些完善,有想了解更多的可以评论里提问哦,我会的一定会给你解答