前言
头部区域完成后,侧边栏区域主要就是展示菜单,之前我们存储了用户动态路由在全局状态中,我们在前面拼接上首页菜单然后还要转化为菜单项的格式。菜单项包含图标和标题,我们需要做对应的处理。内容区域根据当前菜单项展示Layout页面的子路由页面即可
Layout模块
侧边栏区域
实现svg图标组件
由于Antd
的图标没法完全满足我的需求,所以菜单的图标我是自己封装了svg
图标组件去实现的。
1.安装svg插件
css
npm install svg-sprite-loader svgo-loader --dev
svg-sprite-loader
会把SVG
塞到一个个symbol
中,合成一个大的SVG
。最后将这个SVG
放入到body中,symbol的id
如果不特别指定,就是你的文件名。
svgo-loader
是基于SVG Optimizer
的一个加载器,而SVG Optimizer
是一个基于node.js
的工具,用于优化SVG
矢量图形文件,它可以删除和修改SVG
元素,折叠内容,移动属性等
2.准备好svg文件
3.craco.config.js配置以及SvgIcon组件封装
安装好之后,我们还得在webpack
的配置文件中定义loader,这个我们直接在craco.config.js
文件下定义就行
craco.config.js
javascript
const path = require('path')
const resolve = (dir) => path.resolve(__dirname, dir)
module.exports = {
webpack: {
alias: {
'@': resolve('src')
},
configure: (webpackConfig, { env, paths }) => {
webpackConfig.module.rules[1].oneOf = [
...[
{
test: /.svg$/,
// 存放svg的文件夹
include: resolve('./src/assets/Icon/svg'),
use: [
{ loader: 'svg-sprite-loader', options: {} },
{ loader: 'svgo-loader', options: {} }
]
}
],
...webpackConfig.module.rules[1].oneOf
]
return webpackConfig
}
}
}
现在为了不每次都写svg标签,我们可以用封装一个组件来使用svg文件
src/components/SvgIcon/index.jsx
javascript
import React from 'react'
import './SvgIcon.scss'
const SvgIcon = React.memo(({ width, height, name, color, className }) => {
return (
<svg className={className || 'icon-svg'} aria-hidden="true" width={width} height={height}>
<use xlinkHref={'#icon-' + name} fill={color}></use>
</svg>
)
})
export default SvgIcon
这样子编写例如<SvgIcon name="404" />
就能够使用
4.全局导入所有svg文件
但是SvgIcon
组件只能一个一个导入svg文件访问,能不能一次性导入全部呢?这里我们可以在assets的Icon文件夹下新建index.js
文件来导入所有svg
文件。这里我们使用到的是webpack
方法是require.context(directory,useSubdirectories = false, regExp = /^.//))
directory
:要引入的文件目录
useSubdirectories
:代表是否查询目录下的子目录
regExp
:匹配要引入文件的正则表达式返回值为一个函数,函数接收一个request参数。执行后返回request所对应文件暴露的Module对象。而返回值函数也是一个对象,对象有三个属性:
resolve
:是一个函数,它返回request被解析后得到的模块id
keys
:也是一个函数,返回的是匹配成功模块的名字组成的数组
id
:context module的模块id经常用到的其实是keys()函数返回的数组
我们依据这个来编写一个方法,参数为require.context
的返回值。
javascript
// 使用 require.context 获取指定文件夹下的所有 SVG 文件
const importAll = (r) => {
// r为require.context执行后的返回函数,接收一个request参数,同时也是一个对象,有keys、resolve、id属性
const svgs = {}
r.keys().map((key) => {
// 用svg对象接收svg文件暴露的Module对象
console.log(r(key));
return (svgs[key] = r(key))
})
return Object.keys(svgs)
}
const iconList = importAll(require.context('@/assets/Icon/svg', false, /.svg$/))
// 获取图标名称为icon-(*).svg数组, 例如[icon-shouye, icon-xitong, icon-zhedie, ...]
export const getNameList = () => {
const regex = /icon-(.*?).svg/
return iconList.map((item) => item.match(regex)[1])
}
得到Module
对象就是如下的结构
然后我们在入口文件中导入这个index文件
src/index.js
arduino
// 全局导入svg图标
import '@/assets/Icon'
我们就可以在body
中看到所有svg文件被导入了
转换菜单数据结构
侧边栏用得是Menu组件,与下拉菜单类似,items
数据用来指定菜单项。我们就先设置一个方法转化一下数据格式。要递归遍历嵌套的路由,有子菜单就递归,无则直接返回单个菜单。
把全局状态存储的React Router结构
{ path, element, title, children }
转换成Antd Menu
提供的方法getItem(label,key,icon,children,type){key,icon,children,label,type}
返回值结构,用到了递归,具体可以参考Ant Design Menu的示例
src/utils/common.js
typescript
/**
* 内置一些工具类函数
*/
// 导入图标组件
import SvgIcon from '@/components/Icon/SvgIcon'
/** 获取菜单项 */
export function getItem(label, key, icon, children, type) {
return { key, icon, children, label, type}
}
/**
* 获取侧边栏菜单项
* @param {*} menuData 嵌套的路由数组
* @returns
*/
export const getTreeMenu = (menuData) => {
if (!menuData || !menuData.length) return
const menuItems = menuData.map((item) => {
if (!item.hidden) {
// 如果有子菜单
if (item.children && item.children.length > 0) {
return getItem(
item.title, '/' + item.path,
<SvgIcon name={item.icon ?? 'component'} width="14" height="14" color="#ccc" />,
getTreeMenu(item.children)
)
}
// 无子菜单
return getItem(
item.title,item.redirect,
<SvgIcon name={item.icon ?? 'component'} width="14" height="14" color="#ccc" />
)
}
})
return menuItems
}
在layout组件中渲染侧边栏菜单
我们直接用动态路由的数据作为参数传入上述getTreeMenu
方法拼接上首页的菜单,然后在Antd
的Menu
组件中使用
src/Layout/index.jsx
javascript
/** 侧边栏菜单 */
const permissionRoutes = useSelector((state) => state.permission.permissionRoutes)
const menuItems = useMemo(() => {
return [
getItem(
<Link to="/home">首页</Link>,
'/home',
<SvgIcon name="component" width="14" height="14" color="#ccc"></SvgIcon>
)
].concat(getTreeMenu(permissionRoutes, themeVari))
}, [permissionRoutes, themeVari])
const handleMenuClick = (menuitem) => {
navigate(menuitem.key)
}
...
return(
...
<Sider ...>
...
<Menu theme={themeVari} mode="inline" items={menuItems} onClick={handleMenuClick} />
</Sider>
)
这样侧边栏菜单就能够正常展示
但是现在存在一个情况刷新后当前访问路径的菜单高亮消失并且菜单全部收缩。
解决刷新后的问题
当前访问路径的菜单高亮消失
Antd的Menu组件有一个属性selectedKeys
代表当前选中的菜单项的key的数组
javascript
// 当前访问路径
const { pathname} = useLocation()
...
+ <Menu ... selectedKeys={[pathname]} />
菜单全部收缩
Antd的Menu组件有一个属性openKeys
代表当前展开的SubMenu
菜单项的key的数组
我们获取到当前访问路径,例如说/system/user,system就是我们需要展开的菜单项的key,现在我们就去取出这个key。
javascript
// 获取当前路径数组片段
const pathSnippets=pathname.split('/').filter(i=>i)
// 获取除最后一个元素外的数组,并且每个元素前加上/
const [subMenuKeys, setSubMenuKeys] = useState(pathSnippets.slice(0, -1).map((item) => '/' + item))
+ <Menu openKeys={subMenuKeys}/>
现在刷新后就能菜单就会在当前菜单项的目录上逐级展开了。但又发现了一个新的问题,现在点击下拉菜单没反应了,我们得在Menu
组件添加一个onOpenChange
配置监测下拉菜单改变重新设置下拉菜单key数组
scss
+ <Menu onOpenChange={(openKeys) =>{setSubMenuKeys(openKeys)}}/>
这样侧边栏区域就算完成了
内容区域
内容区域我们直接用<Outlet />
标签展现layout子路由页面即可
css
...
<Content
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: colorBgContainer
}}>
<Outlet />
</Content>
...
参考文章
# 使用 svg-sprite-loader、svgo-loader 优化项目中的 Icon
代码
上述实现的代码都放在react-antd5-admin,大家可自行查阅