环境
"antd": "3.26.20",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"react-scripts": "3.0.1",
功能
- 路由表生成菜单
- 页面刷新展开激活菜单
defaultOpenKeys - 定位
selectedKeys
效果
代码
路由结构
interface Route {
key: string // 路径
meta: {
label: string // 名称
hidden?: boolean // 隐藏
}
children?: Route[] // 子路由
}
路由表 src/config/routes.js
// import Login from '../pages/login'
// import Admin from '../pages/admin'
// import Home from '../pages/home'
// import Category from '../pages/category'
// import Product from '../pages/product'
// import Role from '../pages/role'
// import User from '../pages/user'
// import Bar from '../pages/charts/bar'
// import Line from '../pages/charts/line'
// import Pie from '../pages/charts/pie'
export default [
{ key: '/login', /* value: Login, */ meta: { label: '登录', hidden: true } },
{
key: '/sub1',
// value: Admin,
children: [{ key: '/home', /* value: Home, */ meta: { label: '首页', icon: 'home' } }],
},
{
key: '/sub2',
// value: Admin,
meta: { label: '商品', icon: 'android' },
children: [
{ key: '/category', /* value: Category, */ meta: { label: '品类管理', icon: 'database' } },
{
key: '/product',
meta: { label: '商品管理', icon: 'barcode', isMenuItem: true },
children: [
{ key: '/product/add-update', meta: { label: '添加/修改商品', icon: 'edit' } },
{ key: '/product/:id', meta: { label: '商品详情', icon: 'form' } },
],
},
],
},
{
key: '/sub3',
// value: Admin,
meta: { label: '人员', icon: 'team' },
children: [
{ key: '/role', /* value: Role, */ meta: { label: '角色管理', icon: 'user' } },
{ key: '/user', /* value: User, */ meta: { label: '账号管理', icon: 'robot' } },
],
},
{
key: '/sub4',
// value: Admin,
meta: { label: '统计', icon: 'area-chart' },
children: [
{ key: '/charts/bar', /* value: Bar, */ meta: { label: '柱图', icon: 'database' } },
{ key: '/charts/line', /* value: Line, */ meta: { label: '线图', icon: 'barcode' } },
{ key: '/charts/pie', /* value: Pie, */ meta: { label: '饼图', icon: 'pie-chart' } },
// test
{
key: '/aaaaaaaaaaaaaaaaaaaa',
meta: { label: 'aaaaaaaaaaaaaaaaaaaa', icon: 'team' },
children: [
{ key: '/bbbbbbbbbbbbbbbb ', meta: { label: 'bbbbbbbbbbbbbbbb', icon: 'home' } },
{
key: '/ccccccccccccccccccc',
meta: { label: 'ccccccccccccccccccc', icon: 'user' },
children: [
{
key: '/dddddddddddddddddddddddddd',
meta: { label: 'dddddddddddddddddddddddddd', icon: 'home' },
children: [
{ key: '/eeee', meta: { label: 'eeee', icon: 'home' } },
{
key: '/ffffff',
meta: { label: 'ffffff', icon: 'home' },
children: [
{ key: '/ggggg', meta: { label: 'ggggg', icon: 'home' } },
{ key: '/hhhhhhh', meta: { label: 'hhhhhhh', icon: 'home' } },
],
},
],
},
],
},
],
},
],
},
]
工具方法 src/utils/menu.js(核心算法)
import React from 'react'
import { Link } from 'react-router-dom'
import { Menu, Icon } from 'antd'
const { SubMenu } = Menu
// 因为路径有可能是 '/product/:id' 如效果图展示的路由, 所以需要正则匹配
const _regExpColonStart = /^:[\w-]+$/
const _regExpMatePathname = (routeKey, pathname) => {
const routeKeyArr = routeKey.split('/')
const pathnameArr = pathname.split('/')
if (routeKeyArr.length !== pathnameArr.length) return false
return routeKeyArr.every((item, i) => item === pathnameArr[i] || _regExpColonStart.test(item))
}
// UI: 生成菜单组件
export const getMenuList = (routes) => {
return routes.reduce((result, route) => {
// 隐藏直接跳过
if (route.meta && route.meta.hidden) {
}
//没有children,空数组也算没有
else if (!route.children || !route.children.length || (route.meta && route.meta.isMenuItem)) {
result.push(
<Menu.Item key={route.key}>
<Link to={route.key}>
<Icon type={route?.meta.icon} />
<span>{route?.meta.label}</span>
</Link>
</Menu.Item>,
)
}
// children只有一条,从孙子往后可能多
else if (route.children && route.children.length === 1) {
result.push(...getMenuList(route.children))
}
// children有2条以上
else if (route.children && route.children.length > 1) {
result.push(
<SubMenu
key={route.key}
title={
<span>
<Icon type={route?.meta.icon} />
<span>{route?.meta.label}</span>
</span>
}
>
{getMenuList(route.children)}
</SubMenu>,
)
}
return result
}, [])
}
/**
* 获取所有SumMenu的key
* 对应的效果是Menu全部展开
* 菜单少的时候可以用
* @param {[]} routes 路由表
* @param {[]} arr 收集器, 最开始是空数组
* @returns {[]} arr收集后的结果, 放到 antd Menu defaultOpenKeys 中
*/
export const getAllSubMenuKeys = (routes, arr = []) => {
return routes.reduce((result, route) => {
// 隐藏直接跳过
if (route.meta && route.meta.hidden) return result
// 0
if (!route.children || !route.children.length) return result
// 1 孙子及后代可能多
if (route.children && route.children.length === 1) return getAllSubMenuKeys(route.children, result)
// 2
if (route.children && route.children.length > 1) {
result.push(route.key)
return getAllSubMenuKeys(route.children, result)
}
return result
}, arr)
}
/**
* 获取当前路由所有SumMenu的key
* 应对的效果是激活的当前路由展开(判断最复杂, 文章重要写的就是这个方法!)
* 菜单多的时候可以用
* @param {[]} routes 路由表
* @param {string} pathname react 中的 this.props.location.pathname
* @param {object} _obj 收集器集合
* @param {[]} _obj.resultRoutes 收集route, 最开始是空数组, 不符合要求会清空
* @param {boolean} _obj.find 是否已经找到标记
* @returns {object} _obj 收集后的结果,主要是 _obj.resultRoutes 放到 antd Menu defaultOpenKeys 中
*/
export const getRouteMatched = (routes, pathname, _obj = { resultRoutes: [], find: false }) => {
// debugger
let i = 0 // 下标, 每一列的开始值为0, 用于标记已经比较至当前列的哪一个route了
const length = routes.length // route.children的长度
return routes.reduce((_o, route) => {
if (_o.find) return _o //若已经找到了, 跳过之后的所有判断, 以免清空收集器obj中的数据
// 路由表中有隐藏标记直接跳过
if (route.meta && route.meta.hidden) {
i++ // 当前列的下标 i 自增+1
return _o
}
// {0}没有children的情况
if (!route.children || !route.children.length) {
// 如果找到了!!
if (route.key === pathname || _regExpMatePathname(route.key, pathname)) {
_o.find = true // 标记已经找到
_o.resultRoutes.push(route) // 收集, 面包屑的最后一项
}
// 不相等, 且不是当前列的最后一项
else if (route.key !== pathname && i + 1 !== length) {
i++ // 当前列的下标 i 自增+1
}
// 不相等, 但已经比到最后一项
else if (route.key !== pathname && i + 1 === length) {
_o.resultRoutes = [] // 因为没有找到, 清空数组. 也包括祖先的key, e.g ['银河系', '太阳系', '火星']
}
return _o // 返回引用收集器
}
// {1,}有children的情况
else {
/**
* 也有可能找到, 比如:
* /product 里还有 /product/detail
* /product 里还有 /product/64baffb4068cd6606a85fadf
* /product/:id 匹配 /product/64baffb4068cd6606a85fadf
*/
if (route.key === pathname || _regExpMatePathname(route.key, pathname)) {
_o.find = true // 标记已经找到
_o.resultRoutes.push(route) // 收集, 因为后代可能有命中的, 若没有命中则全部清空
return _o // 既然已经找到了, 虽然还有 children, 但也停止递归了
}
// 不收集只有一个children的route, 不渲染到Menu, 也不渲染到Breadcrumb
if (route.children.length >= 2) {
_o.resultRoutes.push(route) // 收集, 因为后代可能有命中的, 若没有命中则全部清空
}
i++ // 当前列的下标 i 自增+1
return getRouteMatched(route.children, pathname, _o)
}
// return _o
}, _obj)
}
UI组件 src/components/left-nav/index.jsx
import React, { Component } from 'react'
import ReactImg from '../react-img' // 一个 logo 标题, 可以删除
import './index.less'
import { Link, withRouter } from 'react-router-dom'
import { Menu } from 'antd'
import routes from '../../config/routes'
import { getMenuList, getAllSubMenuKeys, getRouteMatched } from '../../utils/menu'
class LeftNav extends Component {
state = {
theme: 'dark',
}
// 性能优化, 一般写同步代码
UNSAFE_componentWillMount() {
this.menuList = getMenuList(routes) // 只在生命周期里渲染一次!
}
render() {
const expandAll = getAllSubMenuKeys(routes)
console.log('全部展开---', expandAll)
const routesInfo = getRouteMatched(routes, this.props.location.pathname)
console.log('展开激活---', routesInfo)
return (
<div>
<Link to="/" className="logo-box">
<ReactImg size={35} />
<span className="logo-txt" style={{ fontSize: '16px', paddingLeft: '6px' }}>
React 16 练习项目
</span>
</Link>
<Menu
theme={this.state.theme}
// defaultOpenKeys 挺复杂, 我用的是低耦合的写法, routes路由表随便写!
// defaultOpenKeys={['/ffffff']} // defaultOpenKeys很笨, 写谁激活谁, 不会激活所有祖先级
// defaultOpenKeys={expandAll} // 获取全部
defaultOpenKeys={routesInfo.resultRoutes.map((route) => route.key)} // 获取激活
selectedKeys={
routesInfo.resultRoutes.map((route) => route.key).splice(routesInfo.resultRoutes.length - 2) // 拿到倒数两项
}
mode="inline"
className="the-menu"
>
{this.menuList}
</Menu>
</div>
)
}
}
export default withRouter(LeftNav)
分部页面路由注册
也可以根据路由表
src/config/routes.js写循环
<Switch>
<Route path="/home" component={Home} />
<Route path="/category" component={Category} />
<Route path="/product" component={Product} />
<Route path="/role" component={Role} />
<Route path="/user" component={User} />
<Route path="/charts/bar" component={Bar} />
<Route path="/charts/line" component={Line} />
<Route path="/charts/pie" component={Pie} />
{/* 为了测试无限递归, 注释掉下面 */}
<Redirect to="/home" component={Home} />
</Switch>
<Switch>
<Route path="/product" exact component={ProductHome} />
<Route path="/product/add-update" exact component={AddUpdate} />
<Route path="/product/:id" component={Detail} />
<Redirect to="/product" />
</Switch>