从零实现一个React+Antd5.0后台管理系统-Layout模块侧边栏和内容区域

前言

头部区域完成后,侧边栏区域主要就是展示菜单,之前我们存储了用户动态路由在全局状态中,我们在前面拼接上首页菜单然后还要转化为菜单项的格式。菜单项包含图标和标题,我们需要做对应的处理。内容区域根据当前菜单项展示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方法拼接上首页的菜单,然后在AntdMenu组件中使用

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,大家可自行查阅

相关推荐
LaughingZhu12 小时前
智能体经典范式构建:ReAct、Plan-and-Solve 与 Reflection
前端·react.js·前端框架
飞天狗14 小时前
长列表滚动卡顿怎么破:虚拟列表选型 + react-virtuoso 实战调优指南
react.js·性能优化
binbin_5216 小时前
React 井字棋教程:用一个小游戏理解组件和状态
前端·react.js·前端框架
张元清17 小时前
React useObjectUrl Hook:预览文件与 Blob,不留内存泄漏(2026)
javascript·react.js
真的想不出名儿21 小时前
Md编辑器整合-react
前端·react.js·编辑器
牛仔只喝牛奶1 天前
多端跨端技术选型:小程序 + H5 + App 的两栈拆分落地
react native·react.js
wear工程师2 天前
setState 到底是不是异步的?这题从 React 16 问到 18,标准答案早就变了
react.js·面试
wanghowie2 天前
LangGraph4j 落地(二)— ReAct 多步工具与 trace 扩展
前端·react.js·前端框架
csj502 天前
前端基础之《React(11)—state声明式》
前端·react.js
张元清2 天前
React useLocalStorage Hook:SSR 安全的持久化状态(2026)
javascript·react.js