一线大厂高级前端编写,前端初中阶面试题,帮助初学者应聘,需要联系微信:javadudu

自定义 Hook 实现 antd 导航菜单项跟随路由 path 高亮的思考

概述

Ant Design 是在 React 中较为流行的前端组件库,而 react-router-dom 是前端路由在 React 下的较为通用的实现方式。

在使用前端路由时,当用户点击刷新、前进或回退按钮而使 URL 变动,前端组件的样式刷新往往需要特别控制。

本文记录在使用 antd 组件 Menu 作为导航菜单与 react-router-dom 协作过程中,遇到的菜单项无法跟随 URL 高亮的问题,思考过程以及解决方案。在最后,将实现一个自定义 Hook 用以包装主要逻辑,以便后续复用。

问题

在给 Menu 组件的 onSelect 中指定 navigate 后,可以正常进行路由切换。但若点击浏览器的刷新,或者前进,回退按钮,Menu 组件的选项高亮会消失,被渲染成从未被点击的状态。

自定义 Hook 实现 antd 导航菜单项跟随路由 path 高亮的思考

如图,点击 Home 按钮后,Home 选项高亮,并跳转至 /home 路径。但此时点击刷新,高亮消失。

解决过程

方案一

修改

首先查阅文档,得知 defaultSelectedKeys 这一属性可控制 Menu 组件中初始选中的菜单项 key 数组。

于是更改代码,首先将对应的 path 作为 Menu 组件中菜单项的 key

const mainMenu = [
  {
    label: 'Home',
    path: '/home',
  },
  {
    label: 'Blogs',
    path: '/blogs',
  },
];

const menuItems: MenuProps['items'] = mainMenu.map((item, index) => ({
  key: item.path,
  label: item.label,
}));

接着使用 useLocation 获取当前 Location 对象,其 pathname 属性即为当前路径,由于先前将菜单项的 key 设为了其对应的路由路径,所以当前 pathname 即为需要高亮的对应菜单项的 key

将 defaultSelectedKeys 属性设为 [pathname],即可使得对应路径的菜单项默认高亮。

  const navigate = useNavigate();
  let { pathname } = useLocation();

  return (
    <div className={style['app']}>
      <header className={style['app-header']}>
        <Menu
          theme='dark'
          mode='horizontal'
          items={menuItems}
          style={{ height: '100%' }}
          onSelect={({ key }) => navigate(key)}
          defaultSelectedKeys={[pathname]}
        />
      </header>
      <section className={style['app-content']}>
        <Outlet />
      </section>
    </div>
  );

进行上述改动后,再次变更路由后刷新,可以看到对应的菜单选项成功高亮。

新的问题

但仔细观察后,发现这一改动并不完善,其产生了新的问题:

自定义 Hook 实现 antd 导航菜单项跟随路由 path 高亮的思考

在使用浏览器的回退按钮时,路由变更,但菜单的高亮选项并未跟随改动。具体原因不明。

但我自己猜测,是 Menu 内部的 state 保存了当前已被选择的菜单项的 key 的数组(以下简称 keys),而路由变更后,虽然 defaultSelectedKeys 发生了更改,但组件内部的 state 中,keys 仍为原来的值,所以高亮显示的仍为先前的选项。

而刷新后,Menu 组件被重新加载,因此其先前的 state 被清除。在新加载的 Menu 组件中,其 state 中保存的 keys 被设置为 defaultSelectedKeys 属性中传入的数组,即由 location.pathname 指定的正确的应该被高亮的菜单项。因此刷新后可以正确显示。

上述猜测目前没有找到证据(如源码实现)的支撑,仅为个人猜测。

方案二

构思

查阅 Ant Design 提供的文档中,关于 Menu 的 API 后,发现有如下的一对属性:

参数 说明 类型
selectedKeys 当前选中的菜单项 key 数组 string[]
onSelect 被选中时调用 function({ item, key, keyPath, selectedKeys, domEvent })

那么或许可以使用 React 中受控组件的思想,对 Menu 菜单项的选中状态进行手动管理。

修改

将当前 selectedKeys 设为组件中的 state,在 Menu 组件的 onSelect 属性中对其进行更改。就如同对于 <input> 中的 valueonChange 一样。

同时,为了保证其与当前 location.pathname 一致,使用 useEffectselectedKeys 进行设置。

代码如下:

const App: React.FC = () => {
  const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
  const navigate = useNavigate();
  let { pathname } = useLocation();

  useEffect(() => {
    setSelectedKeys([pathname]);
  }, [pathname]);

  return (
    <div className={style['app']}>
      <header className={style['app-header']}>
        <Menu
          theme='dark'
          mode='horizontal'
          items={menuItems}
          style={{ height: '100%' }}
          onSelect={({ key }) => {
            navigate(key);
            setSelectedKeys([key]); // 本句可删除
          }}
          selectedKeys={selectedKeys}
        />
      </header>
      <section className={style['app-content']}>
        <Outlet />
      </section>
    </div>
  );
};

此时可以注意到,在 onSelect 中,我们不仅使用了 navigate 对路由进行导航,还使用 setSelectedKeys 更改了当前菜单高亮项。

但其实在 useEffect 中,我们已经根据路由导航的地址更改了菜单的高亮项。因此,此处 setSelectedKeys([key]); 可作为重复逻辑删去。

至此,便完成了菜单项高亮与路由间的联动:

自定义 Hook 实现 antd 导航菜单项跟随路由 path 高亮的思考

思考

问题虽然被解决了,但仍旧存在改进的空间。

  • 先前的解决方案中,使用每个菜单项对应的路由 path 作为该菜单项的 key,这在一级路由中可以很好地起作用。但当存在嵌套路由时,key 中需要包含上一级甚至往上好几级与本层级无关的路由 path 信息,否则无法正确跳转。
  • 这些大段的控制代码是否可以包装为一个自定义 Hook,方便后续在其它情况下调用?

方案三

改进思路

根据先前的思考,或许可以将 key 指定为当前菜单管理的本级路由 path 片段。

例如对于管理 /user/ 下路由的二级菜单,则该菜单中,导向 /user/login 的菜单项的 keylogin,导向 /user/profile 的菜单项 keyprofile。以此类推。

navigate 跳转的目的 path,可根据当前的 path 获取前面层级的 path 信息,结合本层级需要跳转至的路由 path 进行拼接而得到。

捋清逻辑后,可将其包装为自定义 Hook,方便后续使用。

代码实现

根据上述思路,可实现自定义 Hook 代码如下:

// index 为当前菜单管理的路由层级,从 1 开始
export function useMenuRoute(index: number) {
  // key 即为当前菜单高亮项
  const [key, setKey] = useState<string>('');
  const { pathname } = useLocation();
  const navigate = useNavigate();

  // 将 path 拆分为数组
  // 如 /user/profile 将被拆分为 ['', 'user', 'profile']
  const pathItems = pathname.split('/');
  // 获取当前路由层级的 path 片段
  const pathItem = pathItems[index] ?? '';

  // 保证 key 与当前层级路由一致
  useEffect(() => {
    setKey(pathItem);
  }, [pathItem]);

  // 将被返回的工具函数,该函数可设置新的 key 并导航至相应路由
  const setMenuItem = (newItemKey: string) => {
    const parentPath = pathItems.slice(0, index).join('/');

    navigate(`${parentPath}/${newItemKey}`);
  };

  // 返回 key 与工具函数
  return [key, setMenuItem] as const;
}

该自定义 Hook 可按如下方式使用:

// 定义菜单选项
const menuItems: MenuProps['items'] = [
  {
    key: 'home',
    label: 'Home',
  },
  {
    key: 'blogs',
    label: 'Blogs',
  },
];

const App = () => {
  // 该菜单管理一级路由,因此传入的参数为 1
  const [selectedKey, setSelectedKey] = useMenuRoute(1);

  // 在 onSelect 中直接调用 setSelectedKey
  // 同时给 selectedKeys 属性赋值为 [selectedKey]
  return (
    <div className={style['app']}>
      <header className={style['app-header']}>
        <Menu
          theme='dark'
          mode='horizontal'
          items={menuItems}
          style={{ height: '100%' }}
          onSelect={({ key }) => setSelectedKey(key)}
          selectedKeys={[selectedKey]}
        />
      </header>
      <section className={style['app-content']}>
        <Outlet />
      </section>
    </div>
  );
};

总结

本文描述了使用 Ant Design 中 Menu 组件作为导航菜单,在与 react-router-dom 共同使用所遇到的组件渲染错误问题——导航菜单项无法正确高亮。

通过查阅 Ant Design 文档中给出的 API 以及结合个人思考,本文逐步给出并完善解决方案。最后将其封装为 Hook 供后续使用。

或许最后的解决方案仍存在些许漏洞,存在较大的改进空间。望各位读者不吝赐教。

原文链接:https://juejin.cn/post/7212536164057563192 作者:Auhnip

(0)
上一篇 2023年3月21日 上午11:05
下一篇 2023年3月21日 下午3:50

相关推荐

发表评论

登录后才能评论