教你封装一个可以根据文章H标签生成目录的组件~

前言

先给大家看一下我博客中做的目录,线上地址:http://43.138.109.120:4000/#/article/48123226 要能根据文章的H标签生成一个侧边目录栏。好了话不多说,让我们开始代码部分。

代码部分

本文中使用的是Vue3

html 复制代码
<v-md-preview
  ref="articleRef"
  :text="article.articleContent"
></v-md-preview>
<Catalog :domRef="articleRef"></Catalog>

v-md-preview是我用的@kangc/v-md-editor是我用的富文本编辑器,就是展示文章内容的,下文中出现的data-v-md-line是它所生成的属性,不同富文本编辑器会有所不同,不过思想做法是一致的。然后Catalog就是我们要做的目录组件了。

接下来我们开始写Catalog组件,先写好template内容。

html 复制代码
<template>
  <div class="catalog-header">
    <div style="margin-left: 5px;">目录</div>
  </div>
  <div class="catalog-content">
    <div class="catalog-item" v-for="(anchor, index) of titleList" :key="anchor.title"
      :class="currentIndex === index ? 'active' : ''" :style="{ paddingLeft: `${5 + anchor.indent * 15}px` }"
      @click="handleAnchorClick(anchor, index)">
      <a> {{ anchor.title }} </a>
    </div>
  </div>
</template>

引入重要的函数

ts 复制代码
import { useScroll, watchThrottled } from '@vueuse/core';

useScroll: 这个函数用于监听滚动事件。这个对象的属性可以让我们获取到滚动条的当前位置,从而根据滚动位置来更新目录。

watchThrottled:这个函数用于在一定时间内频繁触发的函数上添加节流功能,主要是在滚动事件中触发然后限制更新目录的频率。

声明好需要用的变量

ts 复制代码
const titleList = ref<any>([]); //获取所有H标签的文本内容
const currentIndex = ref(0);    //当前观看部分所属的目录下标
const props = defineProps({     //domRef是要被生成目录的文章的ref
  domRef: {
    type: Object,
    default: null,
  }
});

生成侧边目录

ts 复制代码
const getTitles = () => {
  const anchors = props.domRef.$el.querySelectorAll('h1,h2,h3')
  const titles = Array.from(anchors).filter((t: any) => !!t.innerText.trim())
  if (!titles.length)
    titleList.value = []
  const hTags = Array.from(new Set(titles.map((t: any) => t.tagName))).sort()
  titleList.value = titles.map((el: any, idx: number) => {
    return {
      title: el.innerText,
      lineIndex: el.getAttribute('data-v-md-line'),
      indent: hTags.indexOf(el.tagName),
    }
  })
}
  1. props.domRef.$el.querySelectorAll('h1,h2,h3')是获取文章dom中的h1,h2,h3标签存入anchors。
  2. 使用 Array.from 方法将 anchors 转换为数组,并使用 filter 方法过滤掉没有文本内容的标题元素。将过滤后的结果存储在 titles 变量中。
  3. 使用 Array.from 方法将 titles 数组中的每个标题元素的标签名映射为一个新的数组,然后使用 Set 对象去除重复的标签名。接着使用 sort 方法对去重后的标签名数组进行排序就是把h2, h2, h3, h3, h3, h3, h2这样的数据换成'H2', 'H3'再赋值给hTags。
  4. 使用 map 方法遍历 titles 数组,对于每个标题元素,创建一个新的对象,包含以下属性:
  • title: 标题元素的文本内容。
  • lineIndex: 标题元素上的 data-v-md-line 属性值,表示标题所在的行号。
  • indent: 标题元素的标签名在 hTags 数组中的索引值,表示标题的缩进级别。 像h1的缩进是0,h2的是1,h3的是2

点击锚点目录功能

用户点击锚点时,平滑滚动到对应的标题位置,并将当前索引值更新为传入的 idx

ts 复制代码
function handleAnchorClick(anchor: any, idx: number) {
  const heading = props.domRef.$el.querySelector(`[data-v-md-line="${anchor.lineIndex}"]`)
  if (heading) {
    window.scrollTo({
      behavior: 'smooth',
      top: heading.offsetTop - 40,
    })
    setTimeout(() => currentIndex.value = idx, 600)
  }
}

通过 querySelector 方法在 props.domRef.$el 元素中查找具有指定 data-v-md-line 属性值的标题元素,并将其赋值给变量 heading

判断 heading 是否存在。如果存在,执行以下操作:

  1. 使用 window.scrollTo 方法平滑滚动到标题元素的顶部位置减去 40 像素的位置。这样可以将页面滚动到目标标题的位置。
  2. 使用 setTimeout 函数设置一个延迟时间为 600 毫秒(0.6 秒)的定时器。定时器回调函数会将当前索引值设置为传入的 idx

目录高亮

在页面滚动时,根据滚动位置判断是否需要更新当前索引值,并在组件挂载完成后获取标题数据

ts 复制代码
const { y } = useScroll(window)
watchThrottled(y, () => {
  titleList.value.forEach((e: any, idx: number) => {
    const heading = props.domRef.$el.querySelector(`[data-v-md-line="${e.lineIndex}"]`)
    if (y.value >= heading.offsetTop - 50) // 比 40 稍微多一点
      currentIndex.value = idx
  })
}, { throttle: 200 })
onMounted(() => {
  nextTick(() => {
    getTitles();
  })
});
  1. const { y } = useScroll(window): 这行代码使用了 Vue.js 3.0 引入的 useScroll 函数,它返回一个包含滚动位置的对象 yuseScroll 函数用于监听窗口的滚动事件,并将滚动位置存储在对象 y 中。
  2. watchThrottled(y, () => { ... }, { throttle: 200 }): 这是一个使用自定义钩子函数 watchThrottled 进行节流的代码块。它接受三个参数:滚动位置对象 y、回调函数和选项对象。回调函数会在滚动位置发生变化时被调用,选项对象中的 throttle 属性指定了节流的时间间隔,单位为毫秒。在这里,节流时间设置为 200 毫秒,意味着回调函数每隔 200 毫秒才会被执行一次。
  3. 回调函数中的代码遍历 titleList 数组,对于每个元素 e,通过查询选择器找到对应的标题元素 heading。然后,判断滚动位置是否超过了标题元素的顶部偏移量减去 50,如果超过,则将当前索引值赋给 currentIndex
  4. onMounted(() => { ... }): 这是一个在组件挂载完成后执行的生命周期钩子函数。在这个钩子函数中,使用 nextTick 函数来确保在 DOM 更新后执行 getTitles 函数。

代码样式

scss 复制代码
<style lang="scss" scoped>
.catalog-header {
  display: flex;
  align-items: center;
}
.catalog-content {
  max-height: calc(100vh - 100px);
  overflow: auto;
  margin-right: -16px;
  padding-right: 16px;
}

.catalog-item {
  margin: 5px 0;
  cursor: pointer;
  transition: all 0.2s ease-in-out;
  font-size: 14px;
  padding: 2px 6px;
  overflow: hidden;
  text-overflow: ellipsis;

  &:hover {
    color: #e9546b;
  }
}

.active {
  background-color: #e9546b;
  color: #fff;

  &:hover {
    background-color: #49b1f5;
    color: #fff;
  }
}
</style>

尾语

好了,关于组件的封装就此结束,感谢你看到了最后,希望这篇文章对你有所帮助。然后如果想多了解下我博客里面的东西可以去看看我的前两篇文章,耗时两个多月,我的全栈项目(vue3+nest)完成啦 - 掘金 (juejin.cn)我的全栈(Vue3+Nest)博客项目开源啦 - 掘金 (juejin.cn)

开源地址:github.com/1Telescope1...

相关推荐
程序员黑豆3 分钟前
AI全栈开发之Java:怎么安装JDK
前端·ai编程·全栈
周杰伦fans4 分钟前
AutoCAD C# 二次开发:如何精确监听工作空间切换事件
前端·c#
丷丩19 分钟前
MapLibre GL JS第41课:向地图添加图标
前端·javascript·mapbox·maplibre gl js
英俊潇洒美少年24 分钟前
前端性能优化:非关键脚本/第三方资源异步加载全解(彻底解决首屏阻塞)
前端·性能优化
如果超人不会飞44 分钟前
TinyVue 组件库实战指南:从安装到上手一篇就够了
vue.js
掘金者阿豪1 小时前
终于!我的第二本书正式出版,吃透 Agentic AI 核心不踩坑
javascript·后端
开飞机的舒克_1 小时前
vue3+router动态权限路由
前端·vue.js
VitoChang1 小时前
放弃手搓路由吧!用 SolidStart 搞 SPA,真香
前端
GuWenyue1 小时前
告别JS类型坑!Ts为什么在ai时代逐渐成为"第一"语言
前端·算法·typescript
三乐2281 小时前
事件循环是什么东西,一篇文章带你了解
前端·javascript