Menu菜单组件设计与前端交互优化实战

春莉酱~ 组件 阅读 533
赞 25 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个月接了个后台管理系统的活,UI 框架用的是 Ant Design,菜单这块自然就用了 Menu 组件。说实话,一开始觉得这不就是个现成的组件嘛,配置几个路由、加点图标,搞定收工。结果越做越发现不对劲——需求比想象中复杂多了。

Menu菜单组件设计与前端交互优化实战

客户要求支持三级菜单,而且有些菜单项要动态加载(比如权限不同看到的不一样),还要支持收缩展开、记住上次打开的状态。最头疼的是,菜单数据是从后端接口拿的,不是写死的静态配置。我一开始图省事,直接在 useEffect 里 fetch 数据,然后塞进 Menuitems 属性,结果页面一加载就卡得不行,尤其是菜单项多的时候,浏览器直接卡几秒。

最大的坑:性能问题

折腾了半天才发现,问题出在每次状态更新都会触发整个 Menu 重新渲染。因为我是把菜单数据存在 React 的 state 里,一旦有子菜单展开/收起,或者用户切换了主题色(影响菜单样式),整个菜单树都会被重建。Ant Design 的 Menu 虽然功能全,但内部实现对大型菜单确实不太友好。

开始没想到这么严重,直到 QA 同学提了个 bug:「菜单超过 50 项时,点击展开有明显延迟」。我本地测了下,100 项菜单,展开一个子菜单居然要 300ms+,这肯定不行。

我试过几种方案:

  • React.memo 包裹菜单项 —— 没用,因为 Menu 是整体组件,内部已经做了优化,外部 memo 无效
  • 把菜单数据缓存到 Redux —— 有点用,但首次加载还是慢
  • 改成虚拟滚动?—— 查了文档,Ant Design 的 Menu 不支持虚拟滚动,得自己造轮子,成本太高

最后灵机一动:既然问题出在「每次更新都重建整个菜单树」,那能不能只让变化的部分重新渲染?于是我把菜单拆成了两层:顶层用静态配置(比如首页、设置这些固定入口),动态部分单独用一个子组件,只在需要时才加载。

核心代码就这几行

关键在于「懒加载」和「局部状态隔离」。我把动态菜单封装成一个独立的组件 DynamicMenu,它只负责渲染从 API 拿到的那部分菜单,并且用 useMemo 缓存转换后的 AntD 格式数据。这样即使父组件 re-render,只要菜单数据没变,DynamicMenu 就不会重新计算。

// DynamicMenu.jsx
import { useState, useEffect, useMemo } from 'react';
import { Menu } from 'antd';

const transformToMenuItems = (data) => {
  return data.map(item => ({
    key: item.id,
    label: item.name,
    children: item.children ? transformToMenuItems(item.children) : undefined,
  }));
};

const DynamicMenu = ({ menuData }) => {
  const memoizedItems = useMemo(() => {
    return transformToMenuItems(menuData);
  }, [menuData]);

  return <Menu items={memoizedItems} mode="inline" />;
};

export default DynamicMenu;

然后在主布局里,只在需要时才渲染这个组件:

// Layout.jsx
import { useState, useEffect } from 'react';
import { Menu } from 'antd';
import DynamicMenu from './DynamicMenu';

const Layout = () => {
  const [dynamicMenuData, setDynamicMenuData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 只在首次加载时请求
    fetch('https://jztheme.com/api/menu')
      .then(res => res.json())
      .then(data => {
        setDynamicMenuData(data);
        setLoading(false);
      });
  }, []);

  return (
    <div className="layout">
      <Menu mode="inline" selectable={false}>
        <Menu.Item key="home">首页</Menu.Item>
        <Menu.Item key="profile">个人中心</Menu.Item>
        {/* 静态菜单结束 */}
        {!loading && dynamicMenuData && <DynamicMenu menuData={dynamicMenuData} />}
      </Menu>
    </div>
  );
};

这样改完后,菜单展开速度从 300ms+ 降到了 30ms 以内,基本感觉不到卡顿了。亲测有效。

踩坑提醒:这三点一定注意

除了性能,还有几个小坑差点让我翻车:

  • key 值必须稳定:一开始我用数组索引当 key,结果动态增删菜单时 UI 错乱。后来改成用后端返回的唯一 ID,问题解决。
  • openKeys 和 selectedKeys 要分开管理:AntD 的 Menu 里,openKeys 控制哪些子菜单展开,selectedKeys 控制哪个菜单项高亮。我一开始混在一起处理,导致点击子菜单时父级自动收起了。后来拆成两个 state,分别监听 onOpenChangeonSelect,逻辑清晰多了。
  • 收缩模式下的图标处理:菜单收缩时,AntD 默认只显示图标。但我们的设计要求收缩时显示文字缩写(比如“系统设置”变成“设”)。这得自己 override 样式 + 自定义 render,不能直接用默认行为。

另外,菜单权限控制也费了点功夫。我们是在前端根据角色过滤菜单项,而不是后端返回。好处是灵活,坏处是首次加载还是会拉全量数据(虽然不渲染)。如果数据量特别大,建议还是后端做过滤。

回顾与反思

现在回头看,这个方案虽然解决了性能问题,但也不是完美的。比如动态菜单的加载状态处理得有点糙,目前是整个区域 loading,其实可以按需加载子菜单(点击父级再请求子级)。不过考虑到项目时间紧,而且菜单结构相对固定,就没继续优化。

另外,菜单的「记住上次展开状态」功能也没完全做好。现在是用 localStorage 存 openKeys,但用户切换账号后没清理,偶尔会串数据。这个问题不大,但确实是个隐患,后续版本得补上。

总的来说,这次踩坑让我意识到:看似简单的组件,一旦数据量上去或交互复杂,就得深入看源码、做性能分析。不能光依赖 UI 库的默认行为。AntD 的 Menu 功能强大,但用在大型项目里,一定要做二次封装和优化。

以上是我个人对这个菜单组件的完整折腾过程,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如结合路由守卫做权限校验),后续会继续分享这类博客。希望对你有帮助!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论