实现文章目录索引

668 阅读2分钟

实现文章目录索引

模仿掘金实现文章的目录和索引功能

image-20220727204325283.png

需要实现的功能:

  1. 根据文章列表生成目录结构
  2. 点击目录索引,页面自动滚动到对应区域
  3. 页面滚动时,目录中对应索引自动高亮

根据文章列表生成目录结构这个比较简单,就是对文章列表进行一个遍历,展示段落标题即可。

我们主要看下后两个功能如何实现。

1. 点击目录索引,页面自动滚动到对应区域

我们在渲染文章列表时,针对每一个段落的 dom 元素,都会设置一个唯一的 id 进行标识

image-20220727205525716.png

当我们点击目录标题时,也可以拿到对应段落的 id,然后获取这个 id 对应的 dom 元素,取到其中的 offsetTop 属性,然后使用 Window.scrollTo(),即可滚动到指定位置

<li
  key={cIndex}
  className={linkClassName(id)}
  onClick={() => {
    setActiveCategory(id)
    // 1. 获取 id 对应 dom 元素的 offsetTop
    const ele: HTMLElement = document.querySelector(id)
    const { offsetTop } = ele
    // 2. 根据 offsetTop 滚动到指定位置
    window.scrollTo({
      top: offsetTop,
    })
  }}>
  {category.category_name}
</li>

2. 页面滚动时,目录中对应索引自动高亮

实现这个功能,首先就是需要监听滚动元素的 GlobalEventHandlers.onscroll 事件,滚动事件触发时,我们获取到 event 对象,此时需要获取到 event.target.documentElement.scrollTop ,这个属性就表示我们当前滚动区域到顶部的距离,我们就可以根据这个值来判断当前应该高亮哪个段落的标题。

实现目录索引功能时我们知道每个段落的 dom 元素都有一个 offsetTop 属性,我们可以提前获取每个元素的 offsetTop 属性,用一个对象数组维护 id 和 offsetTop 的映射关系,当滚动事件的 event.target.documentElement.scrollTop 属性某个段落对应的 offsetTop 相等时,就高亮对应段落的目录标题即可。但是滚动事件的触发比较频繁,可能无法刚好与某个 offsetTop 相等,所以设置一个区间更合理。我们取当前段落的 offsetTop 到下一个段落的 offsetTop 为一个区间,当 scrollTop 到达这个区间时,就高亮第一个段落的目录标题。

  // 1. 获取 id 对应的 offsetTop 区间
  useEffect(() => {
    categoryEles.current = [];
    categoryList?.forEach((category, index: number) => {
      const id = `#category-${category.category_id}`;
      const el: HtmlElement = document.querySelector(id);
​
      let categoryTopRange: CategoryTopRange = {
        id,
        start: 0,
        end: 0,
      };
     
      if (index === categoryList.length - 1) {
        categoryTopRange = {
          id,
          start: el.offsetTop,
          end: document.body.offsetHeight, // 最后一个段落的区间 end 就是 body 的高度
        };
      } else {
        const nextCategory: VolumeCategory = categoryList[index + 1];
        const nextEl: any = document.querySelector(
          `#category-${nextCategory.category_id}`
        );
​
        categoryTopRange = {
          id,
          start: el.offsetTop,
          end: nextEl.offsetTop,
        };
      }
      categoryEles.current.push(categoryTopRange);
    });
  }, [categoryList]);
​
  // 2. 监听滚动事件,当滚动到某个区间时,激活对应段落状态
  useEffect(() => {
    const body = document.getElementsByTagName('body')[0];
    console.log(body);
    body.onscroll = (e: any) => {
      if (!ticking.current) {
        window.requestAnimationFrame(function () {
          const top = e.target.documentElement.scrollTop || 0;
          console.log({ top });
          const category: CategoryTopRange | undefined =
            categoryEles.current.find(
              (cate: CategoryTopRange) => cate.start <= top && cate.end > top
            );
          if (category) {
            setActiveCategory((category as CategoryTopRange).id);
          }
          ticking.current = false;
        });
​
        ticking.current = true;
      }
    };
  }, [categoryEles]);

3. 滚动到顶部

这个比较简单

<div
  onClick={() => {
    // 滚动到顶部
    window.scrollTo({
      top: 0,
    })
  }}
>
  <ToTop />
</div>
​

image-20220727213830447.png