前言
在四个月前,我随手帮一个网友完成了一个需求,在 antd pro 中增加多 tabs 布局,详见 网友需求 - 使用 50 行代码在 Ant Design Pro 中完成 Umi 状态保持的多tabs布局
真的只是随手为之,因为多 tabs 在 pro 项目的需求,一直被以“与设计不符”为由拒绝了。(当然我也以这个理由拒绝过产品。)这个讨论由来已久,近期 pro issues 中讨论的也很少,我以为应该已经不会有人需要这样的需求了。
没想到,新的多 tabs 方案发出之后,社区上关于这东西的讨论还挺多的,网友又疯狂的给我提需求:
- 允许设置“首页” tab 不关闭
- 允许动态修改 tab 标题
- 和 pro layout 组合使用
- 允许和 pro 的国际化方案组合使用
- 允许动态设置哪些页面需要放到 tab 里
- 允许 tabs 带 icon
- 增加菜单 - 可关闭左侧 - 可关闭右侧 - 可关闭其它
- 只剩一个 tab 时隐藏关闭按钮
- 等
由于感兴趣的朋友不少,所以在这里重新整理了一下需求和用法,包括为什么这么设计,欢迎各位参与讨论。
共识
这个方案只能用在 umi@4 系列的项目中,比如 alita@3 、 @umijs/max 和 kmi 等。 umi@3 系列的我之前也写过一个 demo 插件,那个不能用了,懒得维护了,所以如果你需要多 tabs 能力,请先升级到 umi@4 。或者自己参考实现写一个 umi@3 的。
因为多 tabs 的基本要求就是在 tab 中的页面,应该被 “状态保持”,因此这个能力的基础是 keepalive,所以实际上这个插件是放到 keepalive 的插件中实现的,tabs-layout 插件只是增加了一个配置,供用户开启和关闭功能使用。所以你会在使用多 tabs 的时候,用到一些 keepalive 插件的 api。
最简单的理解应该是,被“标记为需要状态保持”的页面,会被放入 tabs 中。这个“标记”是 keepalive 实现的。
为什么不独立出来
因为这两个插件共用一个临时文件,是通过同一个 tpl 文件生成的。这玩意拆不开。
这里顺便吐槽一下 tpl 真的是反人类,我和陈杰两个人最近写 tpl 写吐了,你可以看到这阶段我们发了很多包,几乎都是因为 tpl 改错了,改新功能的时候,影响了旧功能。
配置说明
keepalive,
keepalive: [/./],
keepalive 的配置项,支持正则表达式。但是所有的路由正则匹配应该是全小写的,比如不管你的路由是 home
、Home
还是 hoMe
,只有设置 keepalive:[/home/]
才有效。而字符串的配置方式就刚好相反,如果你的路由是home
,你配置 home
、Home
还是 hoMe
都有效。
/./ 表示全匹配,也就是全部的路由都保持
tabsLayout
tabsLayout: {
hasDropdown: true,
},
开启 tabs 功能,hasDropdown 表示是否使用默认的右侧功能,关闭左侧,右侧其它等。
用法
首先你应该合理的把方案集成到你的系统中,请注意区分,你现在用的是什么框架。
我知道你下意识的会回答 “umi” 框架。请按下面分类认证区分。
umi 项目中使用
这里的 umi 项目指的是没有使用多余的插件集合的 umi@4 项目,最好的区分方式是你启动项目时,执行的是 umi dev
。
安装模块
pnpm i @alita/plugins
配置
config/config.ts 或者 .umirc.ts
export default {
plugins: [
require.resolve('@alita/plugins/dist/keepalive'),
require.resolve('@alita/plugins/dist/tabs-layout'),
],
keepalive: [/./],
tabsLayout: {
hasDropdown: true,
},
}
运行时配置
运行时配置,在你需要的时候选用,比如要匹配tabs 的名称和 icons 可以配置 tabsLayout
,像自定义 tabs 时,则配置 getCustomTabs
export async function tabsLayout() {
return {
local:{
'/':'首页',
'/users':'用户',
'/foo':'其他'
},
icons:{
// 组件哦
'/':DesktopOutlined
},
};
}
export const getCustomTabs = (config) => {
// 一些需要的配置
return <>自定义的 tabs 组件<>
}
使用 useKeepOutlets
在你的全局layout 中使用 useKeepOutlets 代替 Outlet
或者 useOutlet
,比如 src/layouts/index.tsx
import { useKeepOutlets } from 'umi';
const Layout = () => {
const element = useKeepOutlets();
return <>{element}</>;
};
export default Layout;
请充分理解上面的用法,这就是这个方案在 umi 插件中需要用到的几处配置,关键有三处,配置 plugins,config 里面配置开关,layout 中使用 useKeepOutlets。
以下几种方式,都是 umi 系的框架,也就是对上面的配置和使用做了隐藏。
alita
在 alita 中内置了这个插件所以你只需要如下使用
有 github 的朋友帮忙点一个 star,想要一个独立完成的 1k star 的项目
安装模块
无需额外安装
配置
因为 alita 中内置 keepalive 和 tabsLayout 插件的引入,所以你不需要额外安装和配置 plugins
export default {
appType: 'pc',
keepalive: [/./],
tabsLayout: {
hasDropdown: true,
},
}
运行时配置
与上面的一致,无特殊点
使用 useKeepOutlets
与上面的一致,无特殊点
@umijs/max
max 系列的项目,因为自带了布局和国际化,初始化数据这些功能,所以多 tabs 插件做了兼容。
安装模块
pnpm i @alita/pluins
配置
config/config.ts 或者 .umirc.ts
export default {
plugins: [
require.resolve('@alita/plugins/dist/keepalive'),
require.resolve('@alita/plugins/dist/tabs-layout'),
],
keepalive: [/./],
tabsLayout: {
hasDropdown: true,
},
// 以下是 max 项目的配置
antd: {},
locale: {
// default zh-CN
default: 'zh-CN',
antd: true,
baseNavigator: true,
},
layout: {
locale: true,
},
model: {},
initialState: {},
}
运行时配置
// 注意这里 传入了 initialState
export async function tabsLayout({ initialState }) {
return {
local:{},
icons:{},
};
}
export const getCustomTabs = (config) => {
// 一些需要的配置
return <>自定义的 tabs 组件<>
}
使用 useKeepOutlets
方法一,与上述相同,与方法二,二选一即可。
方法二,在 layout 的 runtime 配置里面增加
这里是 app.tsx 文件中配置,详见 layout 插件的文档。
import { useKeepOutlets } from '@umijs/max';
export const layout: RunTimeLayoutConfig = ({
initialState,
setInitialState,
}) => {
return {
childrenRender: (_, props) => {
const element = useKeepOutlets();
return (
<>
{element}
</>
);
},
...
};
};
pro 项目
这里的 pro 项目指的是 ant-design-pro,版本是 6.0.0-beta.1
之后。
其实 pro 就是 max 加上了 umi-presets-pro
就是帮你加了一下 plugins 配置,主要增加使用了这几个插件,说明在下文注释里面。
const plugins = [
// 一些 pro 项目需要的配置
require.resolve('./features/proconfig'),
// 默认使用了上面提到的 “使用 useKeepOutlets”
require.resolve('./features/maxtabs'),
// opengapi 不是 多 tabs 需要的
require.resolve('@umijs/max-plugin-openapi'),
// 帮你配置 keepalive 和 tabs-layout
require.resolve('@alita/plugins/dist/keepalive'),
require.resolve('@alita/plugins/dist/tabs-layout'),
];
安装模块
无需额外安装
配置
export default {
// 增加 配置
keepalive: [/./],
tabsLayout: {
hasDropdown: true,
},
}
运行时配置
与 max 系列项目相同
使用 useKeepOutlets
无需其它配置使用,因为在 umi-presets-pro
已经通过插件的方式使用了,做的事情一模一样。
API
API 通过 const { updateTab, dropByCacheKey } = React.useContext(KeepAliveContext);
dropByCacheKey
刷新某个路由
dropByCacheKey(location.pathname)
updateTab
更新 tab 信息,支持更新 icon name 和 关闭按钮
updateTab(location.pathname, {
icon: <UserOutlined />,
name: 'hahaha' + Math.ceil((Math.random() * 100) / 10),
closable: false,
});
以下是个实例,供参考
import { UserOutlined } from '@ant-design/icons';
import { KeepAliveContext, useLocation, useParams } from 'alita';
import { Button } from 'antd';
import React, { useEffect, useState } from 'react';
export default () => {
const [count, setCount] = useState(0);
const location = useLocation();
const { updateTab } = React.useContext(KeepAliveContext);
const params = useParams();
const handleClick = () => {
updateTab(location.pathname, {
icon: <UserOutlined />,
name: 'hahaha' + Math.ceil((Math.random() * 100) / 10),
closable: false,
});
};
return (
<div>
<h3>当前页面状态被设置成自动保存</h3>
<h3>当前计数是:{count}</h3>
<Button
color="primary"
block
size="large"
onClick={() => setCount(count + 1)}
>
点我计数加1
</Button>
<Button onClick={handleClick}>修改tab</Button>
</div>
);
};
总结
好复杂,看不懂,更新 pro 最新的代码,配置使用
keepalive: [/./],
tabsLayout: {
hasDropdown: true,
},
感谢
以上的网友需求,大部分是 chj-damon 完成的,在此特别感谢。
感谢阅读,关于多 tabs ,如果你有什么想说的,欢迎交流。