likes
comments
collection
share

网友需求 - 使用 50 行代码在 Ant Design Pro 中完成 Umi 状态保持的多tabs布局

作者站长头像
站长
· 阅读数 53

Umi@4、Ant Design Pro、多 Tabs 布局、状态保持、50 行代码、这些关键词能组合出一篇你感兴趣的文章吗?

缘由

网友需求 - 使用 50 行代码在 Ant Design Pro 中完成 Umi 状态保持的多tabs布局

其实多标签页这个需求,已经很久远了,在 pro issues #200 就有过这个讨论。

网友需求 - 使用 50 行代码在 Ant Design Pro 中完成 Umi 状态保持的多tabs布局

题外话,截止本文撰写时,现在 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 都会发布新版,大家可以持续关注官网信息哦。

网友需求 - 使用 50 行代码在 Ant Design Pro 中完成 Umi 状态保持的多tabs布局

思路

1、为了避免一些非技术上的问题,新版的 tabs 插件和 keepalive 插件合并在一起实现。因为 keepalive 插件我们内部用到的很多,可以借场景验证整个方案的健壮性。

2、实现上,只有被标记为“需要状态保持”的页面,才会被添加到多 tabs 标签中,可以解 “只有部分页面需要多 tabs 其他页面不需要” 的需求。

3、在状态保持方案的基础上,尽可能少的改动和增加代码来实现想要的需求。

4、为 keepalive 插件,增加一个新的配置用于开启多 tabs 布局

实现

因为 antd 的 Tabs 组件,本身是可以做到,Tabs 切换的时候保持页面数据不变更的。但是基本上平时我们在使用这个组件的时候,都是属于“组件级别”用法,就是在同一个页面中,做多 tabs 切换。因此我们需要将他提升到“页面级别”。

实现也很简单,那就是不把页面放到 TabsTabPane 中。

export function App() {
    return <>
        <div>
            <Tabs >
                {panels.map(() => (
                    <TabPane />
                ))}
            </Tabs>
        </div>
        {children}
    </>
}

要让页面和 Tabs 产生关联,只要将当前页面的路由 location.pathname 和当前所有被状态保持的页面 作为 TabsactiveKey 和渲染 TabPanepanels 数据即可,将组件级别的 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 布局中放一个没用的 useStatekeepElements.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 安装即可。

如果你喜欢这个文章,请一键三连,并分享给需要的朋友。如果你也有一些其他“需求”需要支撑,也可以提供给我,我是一个很乐于帮助网友的人。

后续

6月20号更新 支持自定义渲染 Tabs

网友需求 - 使用 50 行代码在 Ant Design Pro 中完成 Umi 状态保持的多tabs布局

用法

1、更新 @alita/plugins@3.0.0-rc.12

2、配置(config/config.ts)中增加

tabsLayout: {
+    hasCustomTabs: true,
},

3、增加运行态(src/app.tsx)配置 getCustomTabs

会自动传入 isKeep, keepElements, navigate, dropByCacheKey, local, activeKey, 这几个属性用于页面渲染,以下是一个范例:

import { message, Tabs } from 'antd';
import React from 'react';

const { TabPane } = Tabs;

export const tabsLayout = {
  local: {
    '/': '首页',
    '/users': '用户',
    '/foo': '其他',
  },
};

export const getCustomTabs = () => {
  return ({
    isKeep,
    keepElements,
    navigate,
    dropByCacheKey,
    local,
    activeKey,
  }: any) => {
    return (
      <div className="rumtime-keep-alive-tabs-layout" hidden={!isKeep}>
        <Tabs
          hideAdd
          onChange={(key: string) => {
            navigate(key);
          }}
          activeKey={activeKey}
          type="editable-card"
          onEdit={(targetKey: string) => {
            let newActiveKey = activeKey;
            let lastIndex = -1;
            const newPanel = Object.keys(keepElements.current);
            for (let i = 0; i < newPanel.length; i++) {
              if (newPanel[i] === targetKey) {
                lastIndex = i - 1;
              }
            }
            const newPanes = newPanel.filter((pane) => pane !== targetKey);
            if (newPanes.length && newActiveKey === targetKey) {
              if (lastIndex >= 0) {
                newActiveKey = newPanes[lastIndex];
              } else {
                newActiveKey = newPanes[0];
              }
            }
            if (lastIndex === -1 && targetKey === location.pathname) {
              message.info('至少要保留一个窗口');
            } else {
              dropByCacheKey(targetKey);
              if (newActiveKey !== location.pathname) {
                navigate(newActiveKey);
              }
            }
          }}
        >
          {Object.entries(keepElements.current).map(
            ([pathname, element]: any) => (
              <TabPane tab={`${local[pathname] || pathname}`} key={pathname} />
            ),
          )}
        </Tabs>
      </div>
    );
  };
};

为什么要增加 hasCustomTabs 而不是直接判断 getCustomTabs 是否配置?

因为使用 node 层的配置,可以在构建阶段生成不含有任何默认引入的模版,如果是判断 getCustomTabs 是否配置的话,那需要到运行时才能区分,这意味着,不论你有没有自定义,模版中都会有 import Tabs form antd

补充

pro 已经内置了多 tabs,你可以在 pro 直接使用多 tabs

export default {\
keepalive: [/./],\
tabsLayout: {},\
};
转载自:https://juejin.cn/post/7109492504424087566
评论
请登录