从零实现一个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,大家可自行查阅

相关推荐
爱喝奶茶的企鹅1 小时前
Next.js 14 性能优化:从首屏加载到运行时优化的最佳实践
react.js
政采云技术2 小时前
React前端权限管理思路
前端·react.js
傻小胖3 小时前
React 脚手架使用指南
前端·react.js·前端框架
若川4 小时前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
真的很上进14 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
wakangda1 天前
React Native 集成原生Android功能
javascript·react native·react.js
秃头女孩y1 天前
【React中最优雅的异步请求】
javascript·vue.js·react.js
前端小小王2 天前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 天前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
不是鱼2 天前
构建React基础及理解与Vue的区别
前端·vue.js·react.js