前言
上期我们通过递归的方式实现了配置化的菜单栏,但是单纯使用菜单栏还不够,项目中需要有一套路由管理。我们可以引入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.效果
最终效果如下,基本的网页的结构就出来了~
二、结论
以上我们就实现了基本的路由管理,并优化了我们的页面布局。
github链接: menu组件:github.com/qiangguangl…
routes路由配置: github.com/qiangguangl…
app.ts文件: github.com/qiangguangl…
下期预告
下期我们紧随vue的专栏,react实现复杂的表单~