react菜单栏路由管理

1,281 阅读4分钟

前言

上期我们通过递归的方式实现了配置化的菜单栏,但是单纯使用菜单栏还不够,项目中需要有一套路由管理。我们可以引入react-router-dom实现,下面使用的是v6版本,详情参照官网

一、实现

1.实现按名称匹配antd的icon

antd官网上icon图标都是通过标签的方式来使用,但是如果想配置化的话,图标需要支持按名称匹配。可以通过props匹配icon标签。

封装icon组件

import * as Icons from '@ant-design/icons';// 导入所有icon
import React from 'react';

const IconFont = (props: {icon: string}) => {
  const { icon } = props
  const antIcon: { [key: string]: any } = Icons
  return React.createElement(antIcon[icon]) // 按名称匹配创建dom
}

export default IconFont

2.引入react-router-dom库

我们这里使用的v6版本,会与v5版本的api有些许不同,具体可以参照官网

配置router路由

import React from 'react'
// 懒加载组件
const Home = React.lazy(() => import('../pages/home'))
const LoopForm = React.lazy(() => import('../pages/form/loopForm'))
const Inherit = React.lazy(() => import('../pages/components/inherit'))
const Table = React.lazy(() => import('../pages/components/table'))
const Skill = React.lazy(() => import('../pages/skill'))

// v6与v5不同,组件通过element属性接受
const routes = [
    {
        path: '/home',
        element: <Home />
    },
    {
        path: '/form',
        element: <LoopForm />,
        children: [
            {
                path: 'loopForm',
                element: <LoopForm />
            }
        ]
    },
    {
        path: '/components',
        element: <Inherit />,
        children: [
            {
                path: 'inherit',
                element: <Inherit />
            },
            {
                path: 'table',
                element: <Table />
            },
        ]
    },
    {
        path: '/skill',
        element: <Skill />
    },
]

export default routes

app中引入使用

<Router>
    <Suspense>
        <Routes></Routes>
    </Suspense>
</Router>

3.实现menu组件点击跳转

如果想实现menu组件点击具体菜单跳转,我们可以通过react-router-dom库的NavLink标签实现,相当于vue的router-link。

import { NavLink as Link } from 'react-router-dom'

<Link to={value}>{label}</Link>

4.最终实现

以上,我们就可以实现路由跳转了

递归方法

通过getItem获取menu组件的配置项,label可以支持link。

import { NavLink as Link } from 'react-router-dom'
// menu组件递归方法
function getMenuList() {
    let tempMenuList: ItemType[] = [];
    const getList = (list:any, newList: MenuItem[]) => {
        for(let i=0; i<list.length; i++) {
            const { value, label, icon } = list[i] || {};
            const tempBo = list[i].children && list[i].children.length
            // 引入NavLink标签,实现路由跳转
            const it = getItem(tempBo ? label : <Link to={value}>{label}</Link>, value || label, icon && <IconFont icon={icon}></IconFont>);
            newList.push(it)
            routeMap[value] = label
            if(tempBo) {
                const tempItem = newList[i] as item
                tempItem.children = [];
                getList(list[i].children || [], tempItem.children);
            }
        }
    }
    getList(menuList, tempMenuList)
    return {tempMenuList}
}

菜单栏配置

export const menuList = [
    {
        value: '/home',
        label: '首页',
        icon: 'HomeOutlined'
    },
    {
        value: '/form',
        label: '表单系列',
        icon: 'SwitcherOutlined',
        children: [
            {
                value: '/form/loopForm',
                label: '循环表单及校验'
            }
        ]
    },
    {
        value: '/components',
        label: '封装组件',
        icon: 'UngroupOutlined',
        children: [
            {
                value: '/components/inherit',
                label: '二次封装组件功能透传'
            },
            {
                value: '/components/table',
                label: 'table组件',
            }
        ]
    },
    {
        value: '/skill',
        label: '奇技淫巧',
        icon: 'ThunderboltOutlined',
    }
]

app文件使用menu组件

<Router>
    <Sider width={256}>
        <MyMenu bread={getBread}></MyMenu>
    </Sider>
    <Suspense>
        <Routes></Routes>
    </Suspense>
</Router>

5.碰到的一些问题

其实以上我们已经可以实现菜单栏点击路由跳转了,但是也会有一些问题

问题1:菜单栏初始高亮问题,初始化页面的时候,高亮的菜单是有问题

问题2:菜单栏默认是全部收缩起来的,体验会不好,如何默认全部打开子菜单

问题3:一些样式优化的问题

针对问题1

我们可以使用antd的menu组件的selectedKeys属性,实现菜单高亮;该属性对应menuItem的key,而我们的key之前已经设置与路由是一致的,那么我们就可以获取当前路由实现默认选中的菜单。可以用react-router-dom库的useLocation的hook实现获取当前路由,当然也可以用location.path获取。

针对问题2

我们可以使用antdmenu组件的defaultOpenKeys属性,传入所有需要展开的菜单项

针对问题3

使用antd的一些布局组件实现

6.最终代码

menu组件

下面我们引入了自定义的icon组件,通过传入的icon属性匹配对应antd的icon;通过openKeys在递归的时候push相应的菜单项,用于展开子菜单;此外,我们还通过子组件向父组件通信的方式,实现了面包屑的效果。

import React, {SetStateAction, useState, memo, useEffect} from "react";
import type { MenuProps } from "antd";
import { Menu } from 'antd';
import { menuList } from './config'
import { ItemType } from "antd/es/menu/hooks/useItems";
import IconFont from './icon';
import { NavLink as Link, useLocation } from 'react-router-dom'

type MenuItem = Required<MenuProps>['items'][number];
function getItem( label: React.ReactNode, key: React.Key, icon?: React.ReactNode, children?: MenuItem[], type?: 'group'): MenuItem {
    return {
      key,
      icon,
      children,
      label,
      type,
    } as MenuItem;
}
interface item {
    key: string,
    icon: string,
    children: item[],
    label: string,
    type: string,
}
let routeMap:{[key:string]:any} = {}
function getMenuList() {
    let tempMenuList: ItemType[] = [];
    let openKeys:string[] = [];
    const getList = (list:any, newList: MenuItem[]) => {
        for(let i=0; i<list.length; i++) {
            const { value, label, icon } = list[i] || {};
            const tempBo = list[i].children && list[i].children.length
            const it = getItem(tempBo ? label : <Link to={value}>{label}</Link>, value || label, icon && <IconFont icon={icon}></IconFont>);
            newList.push(it)
            openKeys.push(value)
            routeMap[value] = label
            if(tempBo) {
                const tempItem = newList[i] as item
                tempItem.children = [];
                getList(list[i].children || [], tempItem.children);
            }
        }
    }
    getList(menuList, tempMenuList)
    return {openKeys, tempMenuList}
}
interface breadType {
    (res: SetStateAction<{
        title: string;
    }[]>): void
}
const MyMenu:React.FC<{bread:breadType}> = (props) => {
    const path = useLocation().pathname
    const [tempPath, setTempPath] = useState(path)
    const { openKeys, tempMenuList } = getMenuList()
    const setBread = (keyPath: string[], props: { bread: any; }, type:boolean) => {
        const items = keyPath.map(v => ({title:routeMap[v]}))
        const newItems = type ? items.reverse() : items
        props.bread(newItems)
    }
    useEffect(() => {
        const initPath = Object.keys(routeMap).reduce((p:string[],c:string):string[] => {
            if(path.match(c)) {
                p.push(c)
            }
            return p
        },[])
        setBread(initPath, props, false)
    },[path, props])
    const onClick:MenuProps['onClick'] = (e) => {
        setTempPath(e.key)
        setBread(e.keyPath, props, true)
    }
    return (
        <Menu
            onClick={onClick}
            style={{width:256, height:'100%'}}
            mode="inline"
            selectedKeys={[tempPath]}
            defaultOpenKeys={openKeys}
            items={tempMenuList}
        ></Menu>
    )
}

export default memo(MyMenu)

app文件

我们还在首页加了header部分,基本构建了网页的布局

import {SetStateAction, Suspense, useState, useCallback} from "react"
import { BrowserRouter as Router, useRoutes } from 'react-router-dom';
import { Layout, Breadcrumb, Image } from 'antd'
import MyMenu from './components/menu'
import routes from './routes/index'
import './style/index.scss'
import picture from './assets/pipi.jpeg' // 引入图片

const { Header, Content, Sider } = Layout;
const Routes = () => {
    const element = useRoutes(routes)
    return element
}
const breadCrumbArr: any[] = []
const App = () => {
    const [breadArr, setBredArr] = useState(breadCrumbArr)
    const getBread = useCallback((res: SetStateAction<{ title: string; }[]>) => {
        setBredArr(res)
    }, [])
    return (
        <Router>
            <Layout className="all_content">
                <Header className="header">
                    <img src={require('./assets/home.png')} alt="加载失败" className="home_img"></img>
                    <i className="home_title">LASSETS</i>
                    <div style={{marginLeft:'auto'}}>
                        <Image className="my_picture" src={picture} alt="屁屁失败"></Image>
                        <span>屁屁</span>
                    </div>
                </Header>
                <Layout>
                    <Sider width={256}>
                        <MyMenu bread={getBread}></MyMenu>
                    </Sider>
                    <Layout>
                        <Breadcrumb style={{margin:'16px 20px 0'}} items={breadArr}/>
                        <Content className="content">
                            <Suspense>
                                <Routes></Routes>
                            </Suspense>
                        </Content>
                    </Layout>
                </Layout>
            </Layout>
        </Router>
    )
}

export default App

7.效果

最终效果如下,基本的网页的结构就出来了~

image.png

二、结论

以上我们就实现了基本的路由管理,并优化了我们的页面布局。

github链接: menu组件:github.com/qiangguangl…

routes路由配置: github.com/qiangguangl…

app.ts文件: github.com/qiangguangl…

下期预告

下期我们紧随vue的专栏,react实现复杂的表单~