路由文件?❎ 菜单文件?❎ 路由菜单二合一文件✅

310 阅读4分钟

老板:小张啊,做一个后台项目,有一些表格表单需要处理。

作为一个专业的切图仔,在老板话音落下的零点零零一秒内,小张就想好了大体设计:

后台管理项目都大差不差,小张依稀记得,需要一个 layout.tsx 文件来存放整体架构,大笔一挥:

export const AppPage = () => {
  return (
    <div className="h-full">
        <Header></Header>
        <div className="flex h-full pt-[60px]">
            <Aside></Aside>
            <Mainer></Mainer>
        </div>
    </div>
  );
};

整体框架就这么勾勒出来了,下一步就是在 Aside 显示菜单,随便从组件库 copy 了一份改造:

<Menu>
    <SubMenu key='/menu1' title="层级1">
      <MenuItem key='/menu1/child1'>Menu 1</MenuItem>
      <MenuItem key='/menu1/child2'>Menu 2</MenuItem>
      <MenuItem key='/menu1/child3'>Menu 3</MenuItem>
      <MenuItem key='/menu1/child4'>Menu 4</MenuItem>
    </SubMenu>
    <SubMenu key='/menu2' title="层级2">
      <MenuItem key='/menu2/child1'>Menu 1</MenuItem>
      <MenuItem key='/menu2/child2'>Menu 2</MenuItem>
      <MenuItem key='/menu2/child3'>Menu 3</MenuItem>
    </SubMenu>
</Menu>

但凭着前端多年的经验,小张改造了一下,将菜单做成了动态的

const menuData = [
  {
    key: 'menu1',
    title: '层级1',
    children: [
      { key: 'child1', label: 'Menu 1' },
      { key: 'child2', label: 'Menu 2' },
      { key: 'child3', label: 'Menu 3' },
      { key: 'child4', label: 'Menu 4' },
    ],
  },
  {
    key: 'menu2',
    title: '层级2',
    children: [
      { key: 'child1', label: 'Menu 1' },
      { key: 'child2', label: 'Menu 2' },
      { key: 'child3', label: 'Menu 3' },
    ],
  },
];

// 动态菜单组件
const DynamicMenu = () => {
  return (
    <Menu>
      {menuData.map((submenu) => (
        <SubMenu key={submenu.key} title={submenu.title}>
          {submenu.children.map((item) => (
            <MenuItem key={item.key}>{item.label}</MenuItem>
          ))}
        </SubMenu>
      ))}
    </Menu>
  );
};

整理了一下,代码优雅了不少,而且扩展性很好。不过除了写菜单,还要写 标签才行

import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

// 假设这里是对应的组件,需要根据实际情况创建或导入
import Menu1Child1Component from './components/Menu1Child1Component';
import Menu1Child2Component from './components/Menu1Child2Component';
import Menu1Child3Component from './components/Menu1Child3Component';
import Menu1Child4Component from './components/Menu1Child4Component';
import Menu2Child1Component from './components/Menu2Child1Component';
import Menu2Child2Component from './components/Menu2Child2Component';
import Menu2Child3Component from './components/Menu2Child3Component';

const AppRouter = () => {
    return (
        <HashRouter>
            <Routes>
                {/* 层级1的路由 */}
                <Route path="/menu1/child1" element={<Menu1Child1Component />} />
                <Route path="/menu1/child2" element={<Menu1Child2Component />} />
                <Route path="/menu1/child3" element={<Menu1Child3Component />} />
                <Route path="/menu1/child4" element={<Menu1Child4Component />} />
                {/* 层级2的路由 */}
                <Route path="/menu2/child1" element={<Menu2Child1Component />} />
                <Route path="/menu2/child2" element={<Menu2Child2Component />} />
                <Route path="/menu2/child3" element={<Menu2Child3Component />} />
            </Routes>
        </HashRouter>
    );
};

export default AppRouter;

最后需要在 Mainer 里显示路由内容

import { Outlet } from 'react-router-dom';

export const Mainer = () => {
    return <Outlet />;
}

这样就完成了完整的一整套路由逻辑了。不过每次添加路由的时候,都需要处理两个地方,一个是 menuData,一个是路由文件。这很不优雅,万一新来了同学,不知道要添加两个东西,出了问题就总会来问,非常麻烦,或许应该将他们收束起来。

这个收束问题可以分为两步走:

  1. 将路由也改成动态的
  2. 用动态路由去转化出 menuData

有同学可能会问,为什么不是由菜单转化为路由呢?我认为菜单的所有选项应该都存在于路由中,但路由中的很多东西往往都不在菜单项中。例如 404 页面,不在菜单项中存在,但是一定在路由里存在,无法由小补大,只能由大滤小。

事已至此,先动态化吧。这里考虑到层级问题,需要小小递归一下

export interface RouteItem {
  path: string;
  element?: ReactElement;
  children?: RouteItem[];
}

const ROUTER_CONFIG = [
    {
        path: "/menu1",
        element: [
            { path: "/child1", element: Menu1Child1Component },
            { path: "/child2", element: Menu1Child2Component },
            { path: "/child3", element: Menu1Child3Component },
            { path: "/child4", element: Menu1Child4Component }
        ]
    },
    {
        path: "/menu2",
        element: [
            { path: "/child1", element: Menu2Child1Component },
            { path: "/child2", element: Menu2Child2Component },
            { path: "/child3", element: Menu2Child3Component }
        ]
    }
];

export const renderRoute = (
  routes: RouteItem[],
  parentPath: string = ""
): ReactNode => {
  return (
    <>
      {routes.map((item) => {
        const { path, element, children } = item;
        const fullPath = parentPath ? `${parentPath}/${path}` : path;
        return (
          <Route key={fullPath} path={path} element={element}>
            {children && renderRoute(children, path)}
          </Route>
        );
      })}
    </>
  );
};

// 使用的时候就可以直接这样
<Routes>{renderRoute(ROUTER_CONFIG)}</Routes>

第二步就是利用这个 ROUTER_CONFIG 来改造出菜单了。但是观察菜单,还需要额外菜单中文名字,那我们就拓展一下 RouteItem

interface Menu {
  title: string; 
  // 因为有可能还有 icon 之类的菜单项特有的东西,我们将其统一抽出来成 Menu
}

export interface RouteItem {
  path: string;
  element?: ReactElement;
  children?: RouteItem[];
  menu?: Menu;
}

由此一来,就可以写个渲染函数来渲染菜单了

// arco design
const renderMenuItems = (items: RouteItem[]) => {
  return items.map((item) => {
    if (item.children?.length) {
      return (
        <Menu.SubMenu
          key={item.path}
          title={item.menu?.title}
        >
          {renderMenuItems(item.children)}
        </Menu.SubMenu>
      );
    }
    return (
      <Menu.Item key={item.path}>
        {item.menu?.title}
      </Menu.Item>
    );
  });
};

// ant design
const renderMenuItems = (
  items: RouteItem[],
  parentPath = ""
): MenuItem[] => {
  const result = items
    .filter((item) => item.menu && !item.menu?.hidden) // 过滤隐藏菜单项
    .map((item) => {
      const { menu, path, children } = item;
      const fullPath = parentPath ? `${parentPath}/${path}` : path;
      const result: MenuItem = {
        key: fullPath,
        label: menu?.title,
        icon: menu?.icon,
        children: children ? renderMenuItems(children, fullPath) : undefined,
      };
      return result;
    });
  return result;
};

注意,需要考虑路由前缀时,还需要处理一下之前的 ROUTER_CONFIG,将其路由进行一次递归拼接,再传到 renderMenuItems 里使用

经过这样的改造,同事只需要按定好的格式填写 ROUTER_CONFIG 即可生成路由 + 菜单,优雅,太优雅了。


我:做的这么优雅,老板一定会刮目相看,从此升职加薪,担任ceo,走向人生巅峰

老板:登录页都没做,这次绩效减半

我:

image.png