教你封装一个可以根据文章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 函数,它返回一个包含滚动位置的对象 y。useScroll 函数用于监听窗口的滚动事件,并将滚动位置存储在对象 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...

相关推荐
用户27050777252413 分钟前
MCP 最小 Server 实测:3 个 JSON-RPC 报错与修法
前端
用户4993350250715 分钟前
一人VIP账号多人共用:Web 代理里的会话共享怎么实现
前端
用户老李17 分钟前
无线电自学教程 05:第一台收音机——从矿石机到 SDR
前端
IPIPD24 分钟前
Node.js请求遇到503怎么重试?先解析Retry-After,再做有上限退避
前端
葡萄城技术团队31 分钟前
dsh-spreadjs-editor 的集成链路:把 SpreadJS Designer 嵌进 Agent 工作台
前端
FlyWIHTSKY1 小时前
vue2和vue3有什么区别,用法上有哪些区别
前端·javascript·vue.js
码云之上1 小时前
10 颗一模一样的「Edit」:中后台 Web Agent 怎么找到对的那一行
前端·人工智能·浏览器
量化分析码农2 小时前
【Python量化多因子实战 04】因子暴露不是 alpha:市值与行业中性化实战
前端·后端
不可能片场2 小时前
Electron 发布 正文写入的三条通道与读回口径
前端·electron
程序员大阳2 小时前
副队长JavaScript教程(66)-本地存储实战:待办事项网页的开发
javascript·json·实战·待办事项·本地存储·案例