在 Umi 社区的四年多,很多交流都是在聊天窗口中进行的,并没有留下太多有价值的内容,虽然我尽量整理了部分内容放到了官网的 FAQ 中,但其实还是有很多内容被遗忘了,所以一直想写这么一个系列。刚好在 Umi 4 开发实战小册[1] 中网友 笑神堕落[2] 问我 Umi 新版会不会支持多标签页?看最近 Umi 团队内的讨论,没有这个需求的讨论,因此我回复了应该不会,但我会做。
01.jpg
其实多标签页这个需求,已经很久远了,在 pro issues #200 就有过这个讨论。
200.jpg
题外话,截止本文撰写时,现在 pro 的 issues 已经 9919。
如果你感兴趣,可以在 antd pro 的issues 上搜索 “tabs” 可以看到相当多的讨论,其实我在 umi@3 的时候,就实现过一个版本,因为和官方内置的 layout 插件,有点使用上的冲突,也因为用的人较少,我自己主要是在移动端h5上面的使用需求较多,所以 umi@3 中的多 tabs 实现,是从 keepalive 复制后修改的一个版本。
有几个历史因素,导致 umi@3 多 tabs 插件比想象中的难用,其中最主要的一个点就是 umi@3 中的 keepalive 插件本身的实现,也是不够优秀的。
但是到了 umi@4 ,react-route 更新到 6 版本之后,陈俊宇给我重写了一遍 keepalive demo,我修改后作为新版的 keepalive 插件,不论从代码的清晰度上还是方案的理想化上,都达到了让我很满意的程度。
因此,对于新版本的 tabs 插件,也是在我原定计划之内会完成的内容,借着今天修改一个 alita 框架的 bug 的空档,顺便把这个需求做了。
先直接上效果吧,这是在 pro 中的实现效果,对了,近期 umi 和 pro 都会发布新版,大家可以持续关注官网信息哦。
layout.png
1、为了避免一些非技术上的问题,新版的 tabs 插件和 keepalive 插件合并在一起实现。因为 keepalive 插件我们内部用到的很多,可以借场景验证整个方案的健壮性。
2、实现上,只有被标记为“需要状态保持”的页面,才会被添加到多 tabs 标签中,可以解 “只有部分页面需要多 tabs 其他页面不需要” 的需求。
3、在状态保持方案的基础上,尽可能少的改动和增加代码来实现想要的需求。
4、为 keepalive 插件,增加一个新的配置用于开启多 tabs 布局
因为 antd 的 Tabs
组件,本身是可以做到,Tabs
切换的时候保持页面数据不变更的。但是基本上平时我们在使用这个组件的时候,都是属于“组件级别”用法,就是在同一个页面中,做多 tabs 切换。因此我们需要将他提升到“页面级别”。
实现也很简单,那就是不把页面放到 Tabs
的 TabPane
中。
export function App() {
return <>
<div>
<Tabs >
{panels.map(() => (
<TabPane />
))}
</Tabs>
</div>
{children}
</>
}
要让页面和 Tabs
产生关联,只要将当前页面的路由 location.pathname
和当前所有被状态保持的页面
作为 Tabs
的 activeKey
和渲染 TabPane
的 panels
数据即可,将组件级别的 change activeKey
直接用页面跳转方法代替。
export function App() {
return <>
<div>
<Tabs onChange={(key: string) => {
navigate(key);
}} activeKey={location.pathname} >
{Object.entries(keepElements.current).map(([pathname]: any) => (
<TabPane tab={`${local[pathname] || pathname}`} key={pathname} />
))}
</Tabs>
</div>
{children}
</>
}
将 Tabs
的关闭行为和状态保持中的清除缓存方法关联到一起 dropByCacheKey(targetKey);
共同维护一份数据。在 Tabs
中的 onEdit
事件中处理即可,要注意如果关闭的是当前页面,那要自动跳转到上一个页面,如果只剩最后一个页面,则提升用户,“必须保留一个窗口”。实现比较简单,感兴趣的直接看 alita 的仓库吧。
需要注意的是我们清除缓存 dropByCacheKey
修改的是 React.useRef
的对象,直接修改它是不会导致页面重绘的,这会导致,我们的关闭 Tabs
事件虽然消除了缓存,但是需要在下一次进入页面的时候,Tab
才会被移除。这显然和我们的需求不符合。今天因为这个问题卡了我很久,最终只想到一个办法,能接问题,但写法很“丑”。如果有别的大哥有更好的解法,欢迎 PR 指教。
在多 Tabs 布局中放一个没用的 useState
当 keepElements.current
修改时,同步去 setState
,虽然你没用到 state
但是当 state
被修改时页面还是会执行一次重绘,这就可以更新我们的页面了。
最终实现的关键代码不到 50 行,并且逻辑非常的清晰,属于新手向非常友好的源码了,你看几遍就可以看懂的。
import React, { useState } from 'react';
import { useOutlet, useLocation, matchPath, useNavigate } from 'react-router-dom'
import { Tabs, message } from 'antd';
import { getPluginManager } from '../core/plugin';
export const KeepAliveContext = React.createContext({});
const { TabPane } = Tabs;
export function useKeepOutlets() {
const location = useLocation();
const element = useOutlet();
const navigate = useNavigate();
const [panel, setPanel] = useState();
const runtime = getPluginManager().applyPlugins({ key: 'tabsLayout', type: 'modify', initialValue: {} });
const { local } = runtime;
const { keepElements, keepalive, dropByCacheKey } = React.useContext<any>(KeepAliveContext);
const isKeep = isKeepPath(keepalive, location.pathname);
if (isKeep) {
keepElements.current[location.pathname] = element;
}
return <>
<div className="rumtime-keep-alive-tabs-layout" hidden={!isKeep} >
<Tabs hideAdd onChange={(key: string) => {
navigate(key);
}} activeKey={location.pathname} type="editable-card" onEdit={(targetKey: string,) => {
// 部分删除实现略
dropByCacheKey(targetKey);
setPanel(Object.entries(keepElements.current))
}}>
{Object.entries(keepElements.current).map(([pathname, element]: any) => (
<TabPane tab={`${local[pathname] || pathname}`} key={pathname} />
))}
</Tabs>
</div>
{
Object.entries(keepElements.current).map(([pathname, children]: any) => (
<div key={pathname} style={{ height: '100%', width: '100%', position: 'relative', overflow: 'hidden auto' }} className="rumtime-keep-alive-layout" hidden={!matchPath(location.pathname, pathname)}>
{children}
</div>
))
}
<div hidden={isKeep} style={{ height: '100%', width: '100%', position: 'relative', overflow: 'hidden auto' }} className="rumtime-keep-alive-layout-no">
{!isKeep && element}
</div>
</>
}
实现上如此的简单高效,用法上也是很简洁明了的。
安装:
pnpm i @umijs/plugins @alita/plugins
// 版本需要大于'@alita/plugins': 3.0.0-rc.11
Umi 配置:
import { defineConfig } from "umi";
export default defineConfig({
plugins: [
require.resolve("@umijs/plugins/dist/antd"),
require.resolve("@alita/plugins/dist/keepalive"),
require.resolve("@alita/plugins/dist/tabs-layout"),
],
antd: {},
keepalive: [/users/, /foo/],
tabsLayout: {},
});
以上配置,需要非常注意的是 plugins
配置,因为这和你框架中使用到的 Umi 的 Preset
you很大的关系,比如在 alita 中,上面的配置可以简化成
import { defineConfig } from "alita";
export default defineConfig({
appType: 'pc',
antd: {},
keepalive: [/users/, /foo/],
tabsLayout: {},
});
而在 Umi Max 中,则可以不而外引入 require.resolve("@umijs/plugins/dist/antd"),
配置,简化也好,不而外引入也好,其实并不是不需要引入了,只是因为这个插件在插件集中已经包含了。这个在使用 Umi 的时候,需要格外留意,当前项目所使用到的所有插件,有一个命令可以查,umi plugin list
可以列出你现在用到的所有的插件。
keepalive
配置接收一个 字符串或者正则表达式的数组,有个取巧的值是配置 keepalive: [/./]
这样所有的页面都会被状态保持。
因为使用到了 antd
的组件,所以需要使用 antd 的插件,并且 antd
包需要独立安装,因为新版的 Umi 没有内置 antd
了,这会让 Umi 包更小。你只需要执行 pnpm i antd
安装即可。
layout.png
如果你喜欢这个文章,请一键三连,并分享给需要的朋友。如果你也有一些其他“需求”需要支撑,也可以提供给我,我是一个很乐于帮助网友的人。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8