本文所用到的知识:react 相关和 antd 等。
关于 antd 之 Layout.Sider 使用说明请移步这里:https://ant.design/components/layout-cn/
本文实现 sidebar 采取的是 “路由与菜单分开管理” 的理念。这样的好处是:
- 不是所有的路由都需要展示在 sidebar 中;
- 避免给路由增加控制菜单的属性,降低路由与菜单的耦合度,简化路由的维护。
import { Menu, Layout } from 'antd';
import {
DatabaseOutlined,
UnorderedListOutlined,
HistoryOutlined,
TeamOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import Cookies from "js-cookie";
import "./index.less"
const { SubMenu } = Menu;
const { Sider } = Layout
const Sidebar = (props) => {
const {
globalCurrentZoo,
location,
} = props;
const superAdmin = () => {
return ["marry", "lily"]
}
const userName = Cookies.get("username");
const isSuperAdmin = superAdmin().includes(userName);
const { pathname } = location;
const [collapsed, setCollapsed] = useState(false);
const [zoo, setZoo] = useState("");
let selectedKeys = [], openKeys = [];
useEffect(()=>{
globalCurrentZoo && setZoo(globalCurrentZoo.name)
}, [globalCurrentZoo]);
const menuList = [{
title: "鱼类",
key: "/animal/:zoo/fish",
icon: <DatabaseOutlined />,
path: `/animal/${zoo}/fish`,
rule: /\/animal\/[\s\S]+\/fish\/?$/,
},{
title: "鸟类",
key: "/animal/:zoo/birds/:type/list",
icon: <UnorderedListOutlined />,
subMenus: [{
title: "鸡",
key: "/animal/:zoo/birds/chicken/list",
path: `/animal/${zoo}/birds/chicken/list`,
rule: /\/animal\/[\s\S]+\/birds\/chicken\/+/,
},{
title: "鸭",
key: "/animal/:zoo/birds/duck/list",
path: `/animal/${zoo}/birds/duck/list`,
rule: /\/animal\/[\s\S]+\/birds\/duck\/+/,
}]
},{
title: "两栖类",
key: "/animal/:zoo/amphibians",
icon: <HistoryOutlined />,
path: `/animal/${zoo}/amphibians`,
rule: /\/animal\/[\s\S]+\/amphibians\/?$/,
},{
title: "爬行类",
key: "/animal/:zoo/reptiles",
icon: <TeamOutlined />,
path: `/animal/${zoo}/reptiles`,
rule: /\/animal\/[\s\S]+\/reptiles\/?$/,
}];
const managerMenus = () => {
{isSuperAdmin ? (
menuList.push(
{
title: "哺乳类",
key: "/animal/:zoo/mammals",
icon: <DeleteOutlined />,
subMenus: [{
title: "猫",
key: "/animal/:zoo/mammals/cat",
path: `/animal/${zoo}/mammals/cat`,
rule: /\/animal\/[\s\S]+\/mammals\/cat\/?$/,
},{
title: "狗",
key: "/animal/:zoo/mammals/dog",
path: `/animal/${zoo}/mammals/dog`,
rule: /\/animal\/[\s\S]+\/mammals\/dog\/?$/,
}]
}
)
) : ("")}
}
let flag = false;
const matchPath = (arr, path, defaultRes={}) => {
let res = {
key: null,
parentKey: null,
...defaultRes,
}
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if(flag) break;
if(item.rule){
const rule = item.rule
const matched = rule.test(path)
if (!matched) {
// 当遍历到子菜单的最后一项时将parentKey置为null
if(i >= arr.length - 1){
res = { ...res, parentKey: null }
}
continue;
}else{
let key = item.key
if(res.parentKey){
// 多级菜单
res = { ...res, key }
}else{
// 一级菜单
res = { ...res, key, parentKey: null }
}
flag = true;
break;
}
}
if (item.subMenus && Array.isArray(item.subMenus)) {
const parentKey = item.key;
// 递归
const subMenusResult = matchPath(item.subMenus, path, { parentKey })
res = { ...res, ...subMenusResult }
}
}
return res;
}
managerMenus();
const { key, parentKey } = matchPath(menuList, pathname);
key && (selectedKeys = [key]);
parentKey && (openKeys = [parentKey]);
const onCollapse = collapsed => {
setCollapsed(collapsed);
};
return (
<div className="sidebar-wrap" style={
{width: collapsed ? "80px" : "200px"}}>
<Sider
className="sidebar"
collapsible
collapsed={collapsed}
theme="light"
onCollapse={onCollapse}
style={
{position: 'absolute'}}
>
<Menu
className="sidebar-menu"
mode="inline"
defaultSelectedKeys={selectedKeys}
defaultOpenKeys={openKeys}
>
{menuList.map((item, idx) => {
return (
<React.Fragment key={idx}>
{item.subMenus ? (
<SubMenu
key={item.key}
icon={item.icon}
title={item.title}
>
{item.subMenus.map((subItem, subIdx) => {
return (
<Menu.Item key={subItem.key}>
<Link to={subItem.path}>{subItem.title}</Link>
</Menu.Item>
)
})}
</SubMenu>
) : (
<Menu.Item key={item.key} icon={item.icon}>
<Link to={item.path}>{item.title}</Link>
</Menu.Item>
)}
</React.Fragment>
)
})}
</Menu>
</Sider>
</div>
);
}
export default Sidebar;
上述代码完全实现了 sidebar,美中不足是:在封装 matchPath 方法时,需要全局声明一个 flag 变量来控制菜单。这会造成该方法与该组件的强耦合,不方便共用。
于是我对 matchPath 方法做了优化:
const matchPath = (arr, path, defaultRes={}) => {
let res = {
key: null,
parentKey: null,
...defaultRes,
}
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if(res.parentKey) break;
if(item.rule){
const rule = item.rule
const matched = rule.test(path)
if (!matched) {
// 当遍历到子菜单的最后一项时将p置为null
if(i >= arr.length - 1){
res = { ...res, p: null }
}
continue;
}else{
let key = item.key
if(res.p){
// 多级菜单
res = { ...res, key, parentKey:res.p }
}else{
// 一级菜单
res = { ...res, key, parentKey:key }
}
break;
}
}
if (item.subMenus && Array.isArray(item.subMenus)) {
const p = item.key;
const subMenusResult = matchPath(item.subMenus, path, {p})
res = { ...res, ...subMenusResult }
}
}
return res;
}
优化后,实现了无需全局声明一个 flag 变量来控制菜单,可是这样会带来一个负面效果:虽然最终的 selectedKeys 和 openKeys 的值是对的,但是 Menu 拿到正确的 openKeys 后,defaultOpenKeys 却是失效的,不知为何,若有幸被大佬看到,求支点迷津(o^^o)。
还有一种优化思路和大家交流一下:就是在写一个函数,在该函数中调用 matchPath 函数,调用之前声明一个变量 flag,初始值是 false。