需求
- 动态显示隐藏菜单
- 动态修改菜单层级、菜单标题
- 不同权限的人菜单不同
- 隐藏的菜单不能通过 url 访问
目标菜单样例如下:
设计与实现
首先前后端需要约定有哪些用于菜单的页面,并约定好页面的key作为前后端的连接点。前端建立所有页面key与跳转链接、组件的映射,后端返回实际页面key和菜单数据。我们以一个简单的例子作为演示,假设系统有两个页面用于菜单['hooks', 'vue']
。
页面key与跳转链接、组件映射
const routes = {
hooks: { path: `${rootUrl}/hooks`, component: Hooks },
vue: { path: `${rootUrl}/vue`, component: Vue },
};
后端生成菜单数据
我们采用数组来存储菜单数据,menus
对象中的page
属性对应约定好的页面key,title
属性是菜单标题,children
属性对应子菜单。菜单数据示例如下:
const menus = [
{
title: "react文档",
children: [
{
title: "api介绍",
children: [
{
page: "hooks",
title: "hooks使用",
},
],
},
],
},
{
title: "vue概述",
page: "vue",
},
];
渲染菜单
根据后端返回的菜单数据,我们要渲染菜单。最终我们要渲染的菜单如下:
<Menu>
<Menu.SubMenu title="react文档">
<Menu.SubMenu title="api介绍">
<Menu.Item key={uuid()}>
<Link>hooks使用</Link>
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
<Menu.Item key={uuid()}>
<Link>vue概述</Link>
</Menu.Item>
</Menu>
根据观察我们发现可以把数组中的每一项可以看作是一个树,比如react文档
这个树有一个子树api介绍
,api介绍
有子树hooks使用
。要生成树对应的菜单项,只需要先获得所有子树的菜单项,因此可以采用先序遍历实现。实现如下:
class SiderMenu extends React.PureComponent {
renderMenuItem = (menu) => {
if (menu.children && menu.children.length > 0) {
const renderChildrenItems = [];
for (const child of menu.children) {
renderChildrenItems.push(this.renderMenuItem(child));
}
return (
<Menu.SubMenu key={uuid()} title={menu.title}>
{renderChildrenItems}
</Menu.SubMenu>
);
} else {
return (
<Menu.Item key={uuid()}>
<Link to={routes[menu.page].path}>{menu.title}</Link>
</Menu.Item>
);
}
};
render() {
const { menus } = this.props;
return (
<Menu mode="inline" defaultSelectedKeys={["react"]}>
{menus.map((menu) => this.renderMenuItem(menu))}
</Menu>
);
}
}
如果children为空则返回<Menu.Item><Link >{menu.title}</Link></Menu.Item>
;如果children不为空则递归处理所有子树,并返回<Menu.SubMenu{renderChildrenItems}</Menu.SubMenu>
。注意由于key为uuid生成的,每次渲染都会生成新组件。所以此处SiderMenu
设计为PureComponent
,当menus
变化才会重新渲染。
生成返回页面路由
如果后端返回的数据中没有某个页面,则通过地址栏中输入该页面url,不应该进入该页面。我们要得到后端返回的页面,可以采用树的先根遍历,所有叶子节点是后端返回的页面,如果有子节点遍历所有子节点。获取后端实际返回的页面代码如下:
const getValidPages = (menus) => {
const pages = [];
const visitMenu = (menu) => {
if (menu.children) {
for (const child of menu.children) {
visitMenu(child);
}
} else {
pages.push(menu.page);
}
};
for (const menu of menus) {
visitMenu(menu);
}
return pages;
};
根据后端实际返回的页面生成路由:
<Switch>
{Object.keys(filterRoutes).map((page) => {
return (
<Route
key={page}
path={routes[page].path}
component={routes[page].component}
/>
);
})}
</Switch>
总结
本文提出了一种较为灵活的动态菜单生成策略,页面key作为前后端唯一联系点,难点在于根据业务场景抽象出数据结构:树,并根据需求采用合理的递归算法操作树结构。
完整代码:https://github.com/compus135/web-examples/tree/master/src/react/components/compflex/DynamicRouter