教你封装一个可以根据文章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...

相关推荐
OpenTiny社区29 分钟前
Coding Agent 到底是怎么工作的,它又为什么会在复杂代码库里失真?
前端·github·agent
奇奇怪怪的34 分钟前
Graph RAG:知识图谱增强检索
前端
默_笙44 分钟前
🌏 MCP 是 AI 界的 USB-C:一个协议让所有模型都能用上所有工具
前端·javascript
以和为贵1 小时前
🔥前端也能搞懂流式输出:从 SSE 到打字机效果
前端·人工智能·架构
阿成学长_Cain2 小时前
Linux telinit 命令详解:运行级别切换|关机重启|系统维护一站式掌握
linux·运维·前端·网络
Patrick_Wilson2 小时前
最佳实践是有保质期的:从一次 CDN external 白屏事故说起
前端·性能优化·前端工程化
YHHLAI2 小时前
[特殊字符] Agent 智能体开发实战 · 第一课:Tool Use —— 让大模型自动干活
前端·人工智能
nuIl2 小时前
我把 5 个编码 Agent 塞进了一个 npm 包
前端·agent·claude
L-影3 小时前
FastAPI 静态文件:Web 页面的“固定展柜”与“加速引擎”
前端·fastapi
陪我去看海3 小时前
受够了 AI 写完代码留下一堆端口?我做了一个端口管理App!
前端·macos·vibecoding