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

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


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



缘由

在 Umi 社区的四年多,很多交流都是在聊天窗口中进行的,并没有留下太多有价值的内容,虽然我尽量整理了部分内容放到了官网的 FAQ 中,但其实还是有很多内容被遗忘了,所以一直想写这么一个系列。


刚好在 Umi 4 开发实战小册 中网友 笑神堕落 问我 Umi 新版会不会支持多标签页?看最近 Umi 团队内的讨论,没有这个需求的讨论,因此我回复了应该不会,但我会做。

image.png

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

image.png

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

image.png

思路

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

image.png

用法

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 已经升级到 Umi@4 了,你可以在 pro 直接使用多 tabs

// 如果安装最新的 pro-preset 就无需配置 plugins
plugins:[require.resolve('@alita/plugins/dist/keepalive'),
    require.resolve('@alita/plugins/dist/tabs-layout'),],
export default {\
keepalive: [/./],\
tabsLayout: {},\
};
复制代码



目录
相关文章
ant design pro 面包屑和多标签页
ant design pro 面包屑和多标签页
469 0
|
前端开发 JavaScript
【Ant Design Pro】使用ant design pro做为你的开发模板(一)拉取项目
【Ant Design Pro】使用ant design pro做为你的开发模板(一)拉取项目
1309 0
【Ant Design Pro】使用ant design pro做为你的开发模板(一)拉取项目
|
前端开发 JavaScript Java
【Ant Design Pro】使用ant design pro做为你的开发模板(完结篇)上线部署项目
【Ant Design Pro】使用ant design pro做为你的开发模板(完结篇)上线部署项目
843 0
【Ant Design Pro】使用ant design pro做为你的开发模板(完结篇)上线部署项目
|
开发工具 git
【Ant Design Pro】使用ant design pro做为你的开发模板(五)去除无效代码,生成一个清晰的开发模板
【Ant Design Pro】使用ant design pro做为你的开发模板(五)去除无效代码,生成一个清晰的开发模板
739 0
【Ant Design Pro】使用ant design pro做为你的开发模板(五)去除无效代码,生成一个清晰的开发模板
|
4月前
|
小程序 前端开发 API
Ant Design Mini 问题之在微信小程序中,由于不支持slot特性,Ant Design Mini的什么组件功能受到了限制,如何解决
Ant Design Mini 问题之在微信小程序中,由于不支持slot特性,Ant Design Mini的什么组件功能受到了限制,如何解决
116 1
|
API
【Ant Design Pro】使用ant design pro做为你的开发模板(九)开发第一个完整的后台页面(二)
【Ant Design Pro】使用ant design pro做为你的开发模板(九)开发第一个完整的后台页面(二)
862 0
【Ant Design Pro】使用ant design pro做为你的开发模板(九)开发第一个完整的后台页面(二)
Ant Design pro删除模块笔记(二)
Ant Design pro删除模块笔记(二)
90 0
Ant design组件库里面的Dropdown怎么写点击事件?
Ant design组件库里面的Dropdown怎么写点击事件?
|
前端开发 JavaScript
重置ant design得样式
重置ant design得样式
78 0
|
前端开发 JavaScript API
【React工作记录九十九】ant design mobile实现tab滚动效果和闪屏小记
【React工作记录九十九】ant design mobile实现tab滚动效果和闪屏小记
226 0