老板:小张啊,做一个后台项目,有一些表格表单需要处理。
作为一个专业的切图仔,在老板话音落下的零点零零一秒内,小张就想好了大体设计:
后台管理项目都大差不差,小张依稀记得,需要一个 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,一个是路由文件。这很不优雅,万一新来了同学,不知道要添加两个东西,出了问题就总会来问,非常麻烦,或许应该将他们收束起来。
这个收束问题可以分为两步走:
- 将路由也改成动态的
- 用动态路由去转化出 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,走向人生巅峰
老板:登录页都没做,这次绩效减半
我: