使

使用递归在 React 中创建现代动态侧边栏菜单

2025-05-25

使用递归在 React 中创建现代动态侧边栏菜单

在Medium上找到我

在网页中,侧边栏由于其导航功能而成为页面中最有用的组件之一。

今天,我们将使用 React 中的递归构建一个现代化的侧边栏。递归是一种函数反复调用自身直到满足特定条件的技术。本文中使用递归时,需要遵循以下三条递归规则:

  1. 该函数应该具有自毁的条件
  2. 该函数应该有一个基本条件
  3. 该函数应该调用自身

即使侧边栏的关注度并非首位,它对网页而言也至关重要。这是因为它可以帮助用户以不同的方式导航,例如,与逻辑导航菜单不同的是,侧边栏可以显示用户可能感兴趣的内容。

但是,我们为什么要在侧边栏中使用递归呢?这和手动编写侧边栏项目有什么区别?如果你经常浏览互联网,你可能会遇到一些网站的侧边栏,并发现有些侧边栏项目包含子版块。有些网站的侧边栏会根据用户导航到的页面路径隐藏或渲染某些项目。这真是太强大了

例如,如果我们查看下面红色圆圈内的图像,编辑器部分是侧边栏的一个项目,紧随其后的 3 个项目(代码编辑器Markdown文本编辑器)是子部分:

侧边栏示例小节 3 个项目

读完这篇文章,你会发现这个看似复杂的侧边栏实际上只有不到50行代码!什么?!

下面是一个基本示例,说明如何扩展此帖子中的侧边栏组件,使其更加时尚,同时仍保留其简洁的感觉:

使用递归扩展的现代动态侧边栏

不用多说,让我们开始吧!

在本教程中,我们将使用create-react-app快速生成一个 React 项目。

(如果您想从 github 获取存储库的副本,请单击此处)。

继续使用以下命令创建一个项目。在本教程中,我将我们的项目命名为modern-sidebar



npx create-react-app modern-sidebar


Enter fullscreen mode Exit fullscreen mode

完成后进入目录:



cd modern-sidebar


Enter fullscreen mode Exit fullscreen mode

在主条目中,src/index.js我们将稍微清理一下,以便我们可以专注于组件:



import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './styles.css'
import * as serviceWorker from './serviceWorker'

ReactDOM.render(<App />, document.getElementById('root'))

serviceWorker.unregister()


Enter fullscreen mode Exit fullscreen mode

现在创建src/App.js



import React from 'react'

const App = () => <div />

export default App


Enter fullscreen mode Exit fullscreen mode

AppSidebar通过创建来导入和使用我们的组件Sidebar.js,所以让我们继续创建它:



import React from 'react'

function Sidebar() {
  return null
}

export default Sidebar


Enter fullscreen mode Exit fullscreen mode

现在我要安装一个 CSS 库,但实际上,即使没有它,你也可以实现我们即将构建的侧边栏的相同功能。我这样做是因为除了方便使用的图标之外,我还喜欢看到额外的涟漪效果 :)



npm install @material-ui/core @material-ui/icons


Enter fullscreen mode Exit fullscreen mode

安装完成后,我们需要在用户界面中构思一个基础结构,以便构建侧边栏。一个解决方案是使用<ul>渲染列表项 ( ) 的无序列表 ( ) 元素<li>。我们将导入ListListItem从 ,@material-ui/core因为List组件本质上是一个ul元素,ListItem组件本质上是一个li

让我们先在侧边栏中硬编码几个项目,以直观地了解一下这看起来会是什么样子,从而增强我们的信心。有时候,多一点自信就能提高我们的工作效率:



import React from 'react'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'

function Sidebar() {
  return (
    <List disablePadding dense>
      <ListItem button>
        <ListItemText>Home</ListItemText>
      </ListItem>
      <ListItem button>
        <ListItemText>Billing</ListItemText>
      </ListItem>
      <ListItem button>
        <ListItemText>Settings</ListItemText>
      </ListItem>
    </List>
  )
}

export default Sidebar


Enter fullscreen mode Exit fullscreen mode

disablePaddingdense用于稍微缩小每个物品的尺寸,道具button用于添加令人惊叹的涟漪效果)。

这是我们目前所拥有的:

侧边栏项目列表项目在 React 中进行硬编码

现在我们已经增强了信心,让我们继续定义props.items,它将Sidebar消耗以呈现其项目。

话虽如此,我们还需要传入一个itemsprop,它是一个对象数组,用于表示侧边栏菜单中的各项内容。我们希望保持功能尽可能简单,否则组件很快就会变得过于复杂。

让我们首先在App组件中创建项目并将其传递props.itemsSidebar



import React from 'react'
import Sidebar from './Sidebar'

const items = [
  { name: 'home', label: 'Home' },
  { name: 'billing', label: 'Billing' },
  { name: 'settings', label: 'Settings' },
]

function App() {
  return (
    <div>
      <Sidebar items={items} />
    </div>
  )
}

export default App


Enter fullscreen mode Exit fullscreen mode

我们现在将更新Sidebar组件以反映此数组结构:



import React from 'react'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'

function Sidebar({ items }) {
  return (
    <List disablePadding dense>
      {items.map(({ label, name, ...rest }) => (
        <ListItem key={name} button {...rest}>
          <ListItemText>{label}</ListItemText>
        </ListItem>
      ))}
    </List>
  )
}

export default Sidebar


Enter fullscreen mode Exit fullscreen mode

你可能注意到了,我们的侧边栏实在太大了!侧边栏通常占据屏幕的一侧。所以我们要做的就是把它的宽度缩小到合适的大小。我们接下来会给它加上一个“ max-widthof” 200px。所以我们要创建一个div元素来包裹我们的List组件。

我们之所以创建另一个div元素而不是直接在组件上应用样式,List是因为我们不想让它List负责宽度大小。这样,将来我们可以选择将其抽象为可重用的侧边栏组件,使其能够根据List元素的大小自适应任何尺寸

这是Sidebar.js组件:



import React from 'react'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'

function Sidebar({ items }) {
  return (
    <div className="sidebar">
      <List disablePadding dense>
        {items.map(({ label, name, ...rest }) => (
          <ListItem key={name} button {...rest}>
            <ListItemText>{label}</ListItemText>
          </ListItem>
        ))}
      </List>
    </div>
  )
}

export default Sidebar


Enter fullscreen mode Exit fullscreen mode

我们在里面index.css定义了类的 CSS 样式sidebar



.sidebar {
  max-width: 240px;
  border: 1px solid rgba(0, 0, 0, 0.1);
}


Enter fullscreen mode Exit fullscreen mode

Material-UI 实际上使用了他们自己的CSS 样式机制,即 CSS-in-JS 方法。但为了避免不必要的复杂化,本文将只使用常规 CSS。

我们已经可以把它保留到这个基本状态,然后就结束了。但是,它不支持子项。我们希望能够点击侧边栏项,并下拉其子项列表(如果有)。子项有助于通过将其他项目分组到另一个侧边栏部分来组织侧边栏:

带有反应子项目的侧边栏项目

我们支持此功能的方式是,在每个侧边栏项目中允许另一个选项,组件将使用该选项来检测其子项目。(你能感觉到递归的到来吗?)

让我们改变App组件中的项目数组以传入子项目:



import React from 'react'
import Sidebar from './Sidebar'

const items = [
  { name: 'home', label: 'Home' },
  {
    name: 'billing',
    label: 'Billing',
    items: [
      { name: 'statements', label: 'Statements' },
      { name: 'reports', label: 'Reports' },
    ],
  },
  {
    name: 'settings',
    label: 'Settings',
    items: [{ name: 'profile', label: 'Profile' }],
  },
]

function App() {
  return (
    <div>
      <Sidebar items={items} />
    </div>
  )
}

export default App


Enter fullscreen mode Exit fullscreen mode

为了能够渲染侧边栏项目的子项目items,我们必须在渲染侧边栏项目时注意属性:



function Sidebar({ items }) {
  return (
    <div className="sidebar">
      <List disablePadding dense>
        {items.map(({ label, name, items: subItems, ...rest }) => (
          <ListItem style={{ paddingLeft: 18 }} key={name} button {...rest}>
            <ListItemText>{label}</ListItemText>
            {Array.isArray(subItems) ? (
              <List disablePadding>
                {subItems.map((subItem) => (
                  <ListItem key={subItem.name} button>
                    <ListItemText className="sidebar-item-text">
                      {subItem.label}
                    </ListItemText>
                  </ListItem>
                ))}
              </List>
            ) : null}
          </ListItem>
        ))}
      </List>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

现在...看,我们令人眼花缭乱的侧边栏组件!

令人眼花缭乱的反应侧边栏组件可能不是

如果您还没有明白,这不是我们想要实现的侧边栏外观。

现在,由于我们不希望用户点击浏览器上的关闭按钮并且不再回到我们的网站,我们需要想办法让它不仅对眼睛更有吸引力,而且对DOM也更有吸引力。

您可能会问:“ DOM是什么意思?”

嗯,仔细看看,问题就出来了!如果用户点击了子项,渲染子项的父项也会消耗点击处理程序,因为它们是重叠的!这很糟糕,会给用户体验带来一些意想不到的麻烦。

我们需要做的是将父级与其子级(子项)分开,以便它们相邻地渲染它们的子项,这样鼠标事件就不会冲突:



function Sidebar({ items }) {
  return (
    <div className="sidebar">
      <List disablePadding dense>
        {items.map(({ label, name, items: subItems, ...rest }) => (
          <React.Fragment key={name}>
            <ListItem style={{ paddingLeft: 18 }} button {...rest}>
              <ListItemText>{label}</ListItemText>
            </ListItem>
            {Array.isArray(subItems) ? (
              <List disablePadding>
                {subItems.map((subItem) => (
                  <ListItem key={subItem.name} button>
                    <ListItemText className="sidebar-item-text">
                      {subItem.label}
                    </ListItemText>
                  </ListItem>
                ))}
              </List>
            ) : null}
          </React.Fragment>
        ))}
      </List>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

现在我们几乎恢复营业了!

React 中令人眼花缭乱的侧边栏也许如此

从截图来看,我们似乎遇到了一个新问题:子项比顶层项大得多。我们必须想办法检测哪些是子项,哪些是顶层项。

我们可以对此进行硬编码,然后就完成了:



function Sidebar({ items }) {
  return (
    <div className="sidebar">
      <List disablePadding dense>
        {items.map(({ label, name, items: subItems, ...rest }) => {
          return (
            <React.Fragment key={name}>
              <ListItem style={{ paddingLeft: 18 }} button {...rest}>
                <ListItemText>{label}</ListItemText>
              </ListItem>
              {Array.isArray(subItems) ? (
                <List disablePadding dense>
                  {subItems.map((subItem) => {
                    return (
                      <ListItem
                        key={subItem.name}
                        style={{ paddingLeft: 36 }}
                        button
                        dense
                      >
                        <ListItemText>
                          <span className="sidebar-subitem-text">
                            {subItem.label}
                          </span>
                        </ListItemText>
                      </ListItem>
                    )
                  })}
                </List>
              ) : null}
            </React.Fragment>
          )
        })}
      </List>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode


.sidebar-subitem-text {
  font-size: 0.8rem;
}


Enter fullscreen mode Exit fullscreen mode

但是我们的侧边栏组件应该是动态的。理想情况下,我们希望它根据调用者传入的 props 来生成相应的项目。

我们将使用depth侧边栏项目将使用的简单道具,并且根据深度,它们可以相应地调整自己的间距,depth无论它们位于树的多远。我们还将把侧边栏项目提取到它自己的组件中,这样我们就可以增加深度,而不必通过引入状态逻辑使其复杂化。

以下是代码:



function SidebarItem({ label, items, depthStep = 10, depth = 0, ...rest }) {
  return (
    <>
      <ListItem button dense {...rest}>
        <ListItemText style={{ paddingLeft: depth * depthStep }}>
          <span>{label}</span>
        </ListItemText>
      </ListItem>
      {Array.isArray(items) ? (
        <List disablePadding dense>
          {items.map((subItem) => (
            <SidebarItem
              key={subItem.name}
              depth={depth + 1}
              depthStep={depthStep}
              {...subItem}
            />
          ))}
        </List>
      ) : null}
    </>
  )
}

function Sidebar({ items, depthStep, depth }) {
  return (
    <div className="sidebar">
      <List disablePadding dense>
        {items.map((sidebarItem, index) => (
          <SidebarItem
            key={`${sidebarItem.name}${index}`}
            depthStep={depthStep}
            depth={depth}
            {...sidebarItem}
          />
        ))}
      </List>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

那么这里发生了什么?

好吧,我们声明了一些强大的 props 来配置侧边栏的预渲染阶段,例如depthdepthStepSidebarItem它们被提取到其自己的组件中,并在其渲染块中用于depth计算其间距。 越高depth,它们在树中的位置就越深。

这一切都是因为这一行:



{
  items.map((subItem) => (
    <SidebarItem
      key={subItem.name}
      depth={depth + 1}
      depthStep={depthStep}
      {...subItem}
    />
  ))
}


Enter fullscreen mode Exit fullscreen mode

depth1每当新的子项列表变得更深入时,它就会增加。

并且内部存在递归SidebarItem,因为它会调用自身,直到不再有基本情况,换句话说,当数组为空时,这段代码就会自动停止:



{
  items.map((subItem) => (
    <SidebarItem
      key={subItem.name}
      depth={depth + 1}
      depthStep={depthStep}
      {...subItem}
    />
  ))
}


Enter fullscreen mode Exit fullscreen mode

现在让我们测试一下递归侧边栏组件:

src/App.js



const items = [
  { name: 'home', label: 'Home' },
  {
    name: 'billing',
    label: 'Billing',
    items: [
      { name: 'statements', label: 'Statements' },
      { name: 'reports', label: 'Reports' },
    ],
  },
  {
    name: 'settings',
    label: 'Settings',
    items: [
      { name: 'profile', label: 'Profile' },
      { name: 'insurance', label: 'Insurance' },
      {
        name: 'notifications',
        label: 'Notifications',
        items: [
          { name: 'email', label: 'Email' },
          {
            name: 'desktop',
            label: 'Desktop',
            items: [
              { name: 'schedule', label: 'Schedule' },
              { name: 'frequency', label: 'Frequency' },
            ],
          },
          { name: 'sms', label: 'SMS' },
        ],
      },
    ],
  },
]

function App() {
  return (
    <div>
      <Sidebar items={items} />
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

React 递归中的炫目侧边栏

我们已经成功了!

让我们玩一下depthStep并传递一个更高的值:



function App() {
  return (
    <div>
      <Sidebar items={items} />
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

React recursionized 中炫目的侧边栏 #2

结论

您可以选择从GitHub 链接下载 repo ,并查看侧边栏的其他功能。它具有更多精彩的功能,例如在渲染中添加一个附加层(侧边栏部分),从而将(分隔线)用作分隔符、侧边栏展开/折叠、图标等。

我希望您发现这篇文章很有价值,并希望将来能获得更多!

在Medium上找到我

文章来源:https://dev.to/jsmanifest/create-a-modern-dynamic-sidebar-menu-in-react-using-recursion-36eo
PREV
Electron React 使用热重载在 Electron 中创建你的第一个 React 桌面应用程序
NEXT
JavaScript 中的回调与承诺