react-router与antd的Menu组件实现菜单高亮


10/10/2018 js react

关于需求

一般做后台管理系统,左边就是整个系统的入口,即菜单,通过点击菜单来到达具体的页面。所以导航菜单中需要高亮当前菜单,来告知用户当前所处的位置。但是如果用户直接从浏览器地址打开某个页面,这个时候菜单并没有高亮对应项,所以需要实现这个功能。

具体实现

1.本次使用React-router-dom@4.3.1作为前端路由,为了方便,直接使用 HashRouter:

// Layout/index.js

//引入必要的组件

import React, { PureComponent } from "react";
import { NavLink, Route, Switch, Redirect, Link } from "react-router-dom";
import { Menu, Icon } from "antd";

import "assets/layout/index.scss";

const MenuItem = Menu.Item;
const SubMenu = Menu.SubMenu;
1
2
3
4
5
6
7
8
9
10
11
12

2.路由配置

//前端需要维护一份路由表,无论是静态配置亦或是由后端获取,
//antd的Menu组件使用key作为菜单项的唯一标识,这里我们直接使用path作为key(如果是子菜单则使用title作为key)
//当浏览器hash改变后可以更方便的获取到菜单项(注意,key一定不要重复,否则达不到效果)

//这里的菜单配置里子菜单可以任意级
const menuConfig = [
  {
    title: "首页",
    icon: "pie-chart",
    path: "/home"
  },
  {
    title: "购买",
    icon: null,
    children: [
      {
        title: "详情",
        icon: null,
        path: "/buy/detail"
      }
    ]
  },
  {
    title: "管理",
    icon: null,
    children: [
      {
        title: "业绩",
        icon: null,
        children: [
          {
            title: "价格",
            icon: null,
            path: "/management/detail/price",
            children: [
              {
                title: "股价",
                icon: null,
                path: "/management/ddd"
              }
            ]
          }
        ]
      }
    ]
  }
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  1. 组件编写

    1. 实现思路: (1).为了实现不限级别的路由渲染及高亮菜单,我这里使用的是递归实现。

    (2).当浏览器地址改变后要高亮对应菜单项,这个可能是一级菜单,也可能是子级菜单,所以还需要展开相应的子菜单

    (3).监听浏览器地址变化,使用 react-router 渲染的组件在 props 中会接收 history 的对象,这个对象有一个 listen 方法,可以添加自定义监听事件,默认接收参数为一个对象:{hash: "",pathname: "",search: "",state: undefined}

    1. 开始实现
    // 1.先定义一个菜单节点类,在下面初始化路由表数据的时候会用到:
    class MenuNode {
      constructor(menuItem, parent = null) {
        this.key = menuItem.path || menuItem.title;
        this.parent = parent;
      }
    }
    // 2. react组件
    // defaultOpenKeys和defaultSelectedKeys是传递给Menu组件,用于指定当前打开的菜单项
    export default class Index extends PureComponent {
      constructor(props) {
        super(props);
        this.state = {
          defaultOpenKeys: [],
          defaultSelectedKeys: []
        };
        this.menuTree = [];
      }
    
      componentDidMount = () => {
        const history = this.props.history;
        //初始化路由表:
        this.initMenu(menuConfig);
        //在渲染完成后需要手动执行一次此方法设置当前菜单,因为此时不会触发history的listen函数
        this.setActiveMenu(history.location);
        this.unListen = history.listen(this.setActiveMenu);
      };
    
      componentWillUnmount = () => {
        //移除监听
        this.unListen();
      };
    
      //序列化路由表
      initMenu = (config, parent = null) => {
        for (let menuItem of config) {
          if (menuItem.children) {
            //如果menuItem有children则对其children递归执行此方法,并且将当前menuItem作为父级
            this.initMenu(menuItem.children, new MenuNode(menuItem, parent));
          } else {
            //如果这个路由不是没有children,则是一级路由,则直接放入menuTree中
            this.menuTree.push(new MenuNode(menuItem, parent));
          }
        }
        /*menuTree中最终存储的是单个menuNode对象,
        通过判断menuNode是否有效的parent即可判断是一级路由还是子菜单下的路由
        */
      };
    
      //这个方法是实现菜单高亮的核心方法
      setActiveMenu = location => {
        //拿到当前浏览器的hash路径
        const pathname = location.pathname;
        //
        for (let node of this.menuTree) {
          /*使用正则判断当前浏览器path是否与菜单项中的key相匹配,
            此正则可以匹配动态路径(类似于/product/:id这种传参的路由),所以即便是动态路由也能高亮对应菜单
            */
          const isActivePath = new RegExp(`^${node.key}`).test(pathname);
          if (isActivePath) {
            const openKeys = [];
            const selectedKeys = [node.key];
            //判断当前菜单是否有父级菜单,如果有父级菜单需要将其展开
            while (node.parent) {
              openKeys.push(node.parent.key);
              node = node.parent;
            }
            this.setState({
              defaultOpenKeys: openKeys,
              defaultSelectedKeys: selectedKeys
            });
            return;
          }
        }
        //如果一个路由都没有匹配上则关闭菜单
        this.setState({
          defaultSelectedKeys: [],
          defaultOpenKeys: []
        });
      };
    
      //用于渲染路由,通过递归实现任意层级渲染
      renderMenuItem = menuArr => {
        const ret = menuArr.map(item => {
          if (item.children) {
            return (
              <SubMenu title={item.title} key={item.path || item.title}>
                {this.renderMenuItem(item.children)}
              </SubMenu>
            );
          } else {
            return (
              <MenuItem title={item.title} key={item.path}>
                <Link to="item.path">{item.title}</Link>
              </MenuItem>
            );
          }
        });
        return ret;
      };
    
      render() {
        return (
          <div>
            <div>
              <div style={{ width: 150 }}>
                <div>当前菜单:{this.state.defaultSelectedKeys[0]}</div>
                <Menu
                  mode="inline"
                  theme="dark"
                  selectedKeys={this.state.defaultSelectedKeys}
                  openKeys={this.state.defaultOpenKeys}
                  onSelect={this.selectMenuItem}
                  onOpenChange={this.openMenu}
                >
                  {this.renderMenuItem(menuConfig)}
                </Menu>
              </div>
            </div>
            <div id="main">heelo,react</div>
          </div>
        );
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
  2. 效果图

Last Updated: 6/25/2021, 3:12:46 PM