在React中创建一个带有高亮显示目录的方法

727 阅读8分钟

目录为网站浏览者提供了页面内容的摘要,使他们可以通过点击所需的标题快速浏览页面的各个部分。通常情况下,目录是在文档和博客中实现的。

在本教程中,我们将学习如何创建一个粘性目录,它将动态地列出页面上的可用标题,突出显示活动的标题。当我们滚动浏览我们的文章时,当一个标题在屏幕上变得可见时,它将在TOC中被突出显示,如下面的GIF所示。

Highlight TOC Demo

要继续学习本教程,你应该熟悉React和React Hooks。你还应该在你的系统上安装Node.js。本教程的完整代码可以在GitHub上找到。让我们开始吧!

设置React

在本教程中,我创建了一个初始版本,其中包括我们用来创建目录的代码。首先,我们需要克隆这个 repo。要做到这一点,在终端运行以下命令:

$ git clone -b starter https://github.com/Tammibriggs/table-of-content.git

$ cd table-of-content

$ npm install

当我们用$ npm start 命令启动应用程序时,我们应该看到以下页面:

React Starter Repo Text Display

创建一个TOC组件

让我们开始创建我们的TOC组件,它将是粘性的,将位于我们屏幕的右侧。

在我们之前克隆的应用程序中,在src 目录中创建一个TableOfContent.js 文件和一个tableOfContent.css 文件。在TableOfContent.js 文件中添加以下几行代码。

// src/TableOfContent.js
import './tableOfContent.css'

function TableOfContent() {
  return (
    <nav>
      <ul>
        <li>
          <a href='#'>A heading</a>
        </li>
      </ul>
    </nav>
  )
}
export default TableOfContent

在上面的代码中,注意到我们正在将文本包裹在一个锚标签中<a></a> 。在我们的TOC中,我们将添加功能,以便当我们点击一个标题时,它将把我们带到页面上的相应部分。

通过在href 属性中传递我们想要导航的章节的ID,我们可以很容易地用锚标签做到这一点。因此,我们页面上的所有章节都必须包含一个ID,我已经在Content.js 文件中包含了这个ID。

接下来,在tableOfContent.css 文件中添加以下几行代码。

// src/tableOfContent.css
nav {
  width: 220px;
  min-width: 220px;
  padding: 16px;
  align-self: flex-start;
  position: -webkit-sticky;
  position: sticky;
  top: 48px;
  max-height: calc(100vh - 70px);
  overflow: auto;
  margin-top: 150px;
}

nav ul li {
  margin-bottom: 15px;
}

现在,为了显示这个组件,前往App.js 文件,添加以下导入。

import TableOfContent from './TableOfContent';

接下来,修改App 组件,使其看起来像下面这样:

// src/App.js
function App() {
  return (
    <div className="wrapper">
      <Content />
      <TableOfContent />
    </div>
  );
}

有了上面的代码,我们将在我们的应用程序的右侧看到一个粘性组件。

找到页面上的标题

要找到我们页面上的所有标题,我们可以使用querySelectorAll document方法,它返回一个 [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList)代表符合指定选择器组的元素列表。

下面的例子显示了我们将如何使用querySelectorAll 方法。

const headings = document.querySelectorAll(h2, h3, h4)

我们已经指定了h2,h3, 和h4 作为选择器,这些都是文章中可能使用的标题。我们不包括h1 ,因为它主要用于页面的标题,而我们希望我们的TOC只包含我们页面的子章节。

现在为了找到标题,在TableOfContent.js 文件中添加以下导入。

import { useEffect, useState } from 'react';

接下来,在该组件中,在返回语句前添加以下几行代码。

// src/TableOfContent.js
const [headings, setHeadings] = useState([])

useEffect(() => {
  const elements = Array.from(document.querySelectorAll("h2, h3, h4"))
    .map((elem) => ({
      text: elem.innerText,
    }))
  setHeadings(elements)
}, [])

上面的代码将找到我们页面上所有指定的标题元素,然后将文本内容存储在状态中。

在上面的代码中,我们正在使用 [Array.from](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from)方法从querySelectorAll 返回的NodeList 创建一个数组。我们这样做是因为有些函数,比如我们上面使用的map ,并没有在NodeList 。为了方便处理找到的标题元素,我们把它们转换成一个数组。

现在,为了在TOC中显示标题,修改组件的返回语句,使其看起来像以下代码:

// src/TableOfContent.js
return (
  <nav>
    <ul>
      {headings.map(heading => (
        <li key={heading.text}>
          <a href='#'>{heading.text}</a>
        &lt;/li>
      ))}
    </ul>
  </nav>
)

现在,当我们在浏览器中打开这个应用程序时,我们会看到以下情况。

Display Headings TOC

层次结构中的链接和列表标题

现在,当我们点击TOC中的一个标题时,它并没有把我们带到正确的章节。你会注意到,它们都在同一行中,没有指示哪个是主标题或副标题。让我们来解决这个问题。

TableOfContent 组件中,修改useEffect Hook,使其看起来像以下代码。

// src/TableOfContent.js
useEffect(() => {
  const elements = Array.from(document.querySelectorAll("h2, h3, h4"))
    .map((elem) => ({
      id: elem.id,
      text: elem.innerText,
      level: Number(elem.nodeName.charAt(1))
    }))
  setHeadings(elements)
}, [])

除了我们发现的标题中的文本外,我们还将为状态添加一个ID和一个level 属性。我们将把ID传递给TOC文本的锚标签,这样当我们点击它时,我们就会被带到页面的相应部分。然后,我们将使用level 属性,在TOC中创建一个层次结构。

修改TableOfContent 组件的返回语句中的ul 元素,使其看起来像下面这样:

// src/TableOfContent.js
<ul>
  {headings.map(heading => (
    <li
      key={heading.id}
      className={getClassName(heading.level)}
      >
      <a
        href={`#${heading.id}`}
        onClick={(e) => {
          e.preventDefault()
          document.querySelector(`#${heading.id}`).scrollIntoView({
            behavior: "smooth"
          })}}
        >
        {heading.text}
      </a>
    </li>
  ))}
</ul>

在上面的代码中,除了将ID添加到锚标签<a></a>href 属性中,我们还添加了一个onClick 事件,该事件触发后,调用scrollIntoView ,使浏览器顺利滚动到相应的章节。

li 元素中,我们在className 属性中调用getClassName(heading.level) 。我们将使用这个我们很快就会创建的功能,根据level 属性的值来设置不同的类名。因此,我们可以给TOC中的副标题设置不同于主标题的样式。

接下来,为了创建getClassName 功能,在TableOfContent 组件外添加以下代码:

// src/TableOfContent.js
const getClassName = (level) => {
  switch (level) {
    case 2:
      return 'head2'
    case 3:
      return 'head3'
    case 4:
      return 'head4'
    default:
      return null
  }
}

现在,在tableOfContent.css 文件中添加以下几行代码。

// src/tableOfContent.css
.head3{
  margin-left: 10px;
  list-style-type: circle;
}
.head4{
  margin-left: 20px;
  list-style-type: square;
}

有了上面的代码,当我们点击TOC中的一个标题或副标题时,我们就会被带到相应的章节。现在,我们的TOC中的标题有了一个层次结构。

Text Hierarchy TOC

查找并突出显示当前活动的标题

当一个标题在我们的页面上可见时,我们要突出显示TOC中的相应文本。

为了检测标题的可见性,我们将使用Intersection Observer API,它提供了一种监视目标元素的方法,当该元素达到预定义的位置时执行一个函数。

用交叉观察者API观察活动的标题

使用Intersection Observer API,我们将创建一个自定义Hook,它将返回活动标题的ID。然后,我们将使用返回的ID来突出显示我们TOC中的相应文本。

要做到这一点,在src 目录中,创建一个hook.js 文件,并添加以下几行代码。

// src/hooks.js
import { useEffect, useState, useRef } from 'react';

export function useHeadsObserver() {
  const observer = useRef()
  const [activeId, setActiveId] = useState('')

  useEffect(() => {
    const handleObsever = (entries) => {}

    observer.current = new IntersectionObserver(handleObsever, {
      rootMargin: "-20% 0% -35% 0px"}
    )

    return () => observer.current?.disconnect()
  }, [])

  return {activeId}
}

在上面的代码中,我们创建了一个新的交叉点观察者的实例。我们传递了handleObsever 回调和一个options 对象,在这里我们指定了观察者的回调在什么情况下执行。

object 使用rootMargin 属性,我们将根元素的顶部缩小20%,也就是目前我们的整个页面,底部缩小35%。因此,当一个标题位于我们页面的顶部20%和底部35%时,它将不会被计算为可见。

让我们指定我们想要观察的标题,把它们传递给交叉点观察者的observe 方法。我们还将修改handleObsever 回调函数,以设置状态中被交叉的标题的ID。

要做到这一点,请修改useEffect Hook,使其看起来像下面的代码。

// src/hooks.js
useEffect(() => {
  const handleObsever = (entries) => {
    entries.forEach((entry) => {
      if (entry?.isIntersecting) {
        setActiveId(entry.target.id)
      }
    })
  }

  observer.current = new IntersectionObserver(handleObsever, {
    rootMargin: "-20% 0% -35% 0px"}
  )

  const elements = document.querySelectorAll("h2, h3", "h4")
  elements.forEach((elem) => observer.current.observe(elem))
  return () => observer.current?.disconnect()
}, [])

TableOfContent.js 文件中,用下面的代码导入创建的Hook。

// src/TableOfContent.js
import { useHeadsObserver } from './hooks'

现在,在TableOfContent 组件中的headings 状态之后调用该Hook。

// src/TableOfContent.js
const {activeId} = useHeadsObserver()

通过上面的代码,当一个标题元素相交时,它将与activeId

突出显示活动的标题

为了突出显示我们TOC中的活动标题,在TableOfContent 组件的返回语句中修改li 元素的锚标签<a></a> ,添加以下样式属性。

style={{
  fontWeight: activeId === heading.id ? "bold" : "normal" 
}}

现在,我们的锚定标签将看起来像下面这样:

// src/TableOfContent.js
<a
  href={`#${heading.id}`} 
  onClick={(e) => {
    e.preventDefault()
    document.querySelector(`#${heading.id}`).scrollIntoView({
      behavior: "smooth"
    })}}
    style={{
      fontWeight: activeId === heading.id ? "bold" : "normal" 
    }}
  >
  {heading.text}
</a>

现在,当一个标题被激活时,它将变成粗体。这样,我们就完成了带有标题高亮的目录的创建。

高亮显示TOC项目的缺点

在为TOC添加项目高亮时,有一些注意事项需要注意。首先,在TOC中添加这一功能并没有标准的方法。因此,在不同的网站上,实现方式是不同的,这意味着我们网站的用户必须学习我们的TOC如何工作。

此外,由于每个目录根据其下的文本,在每个标题之间有不同的间距,我们对高亮功能的实现可能对所有的标题都不准确。

总结

在你的博客或文章中添加一个目录,可以为网站访问者创造更好的体验。在本教程中,我们学习了如何创建一个带有项目高亮的目录,以指示每个活动的标题,帮助你的用户浏览你的网站,改善你的整体用户体验。