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

相关推荐
神夜大侠1 小时前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱1 小时前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号2 小时前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
wyy72932 小时前
v-html 富文本中图片使用element-ui image-viewer组件实现预览,并且阻止滚动条
前端·ui·html
前端郭德纲2 小时前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6
究极无敌暴龙战神X2 小时前
前端学习之ES6+
开发语言·javascript·ecmascript
王解2 小时前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6
欲游山河十万里2 小时前
(02)ES6教程——Map、Set、Reflect、Proxy、字符串、数值、对象、数组、函数
前端·ecmascript·es6
明辉光焱2 小时前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
PyAIGCMaster2 小时前
python环境中,敏感数据的存储与读取问题解决方案
服务器·前端·python