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

相关推荐
云水一下6 小时前
TypeScript 从零基础到精通(五):高级类型与泛型
前端·javascript·typescript
counterxing6 小时前
vibe coding 之后,我更不想打字了
前端·agent·ai编程
云水一下6 小时前
TypeScript 从零基础到精通(六):类型声明与模块化
javascript·typescript
copyer_xyf6 小时前
Python 模块与包的导入导出
前端·后端·python
研☆香6 小时前
es6新特性功能介绍(四)
前端·ecmascript·es6
微扬嘴角7 小时前
React篇1--JSX语法规则、组件、组件实例的3大特性
前端·react.js·前端框架
copyer_xyf7 小时前
Python venv 虚拟环境
前端·后端·python
无聊的老谢7 小时前
Vue 3 + TypeScript 构建大型电信运维平台的前端架构设计
前端·vue.js·typescript
xiaofeichaichai7 小时前
Map / Set / WeakMap / WeakSet
前端·javascript
李可以量化7 小时前
成交量的终极量化策略:价量共振指标完整实现(下篇)
前端·数据库·人工智能