前言
先给大家看一下我博客中做的目录,线上地址: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),
}
})
}
props.domRef.$el.querySelectorAll('h1,h2,h3')
是获取文章dom中的h1,h2,h3标签存入anchors。- 使用
Array.from
方法将anchors
转换为数组,并使用filter
方法过滤掉没有文本内容的标题元素。将过滤后的结果存储在titles
变量中。- 使用
Array.from
方法将titles
数组中的每个标题元素的标签名映射为一个新的数组,然后使用Set
对象去除重复的标签名。接着使用sort
方法对去重后的标签名数组进行排序就是把[h2, h2, h3, h3, h3, h3, h2]这样的数据换成['H2', 'H3']再赋值给hTags。- 使用
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
是否存在。如果存在,执行以下操作:
- 使用
window.scrollTo
方法平滑滚动到标题元素的顶部位置减去 40 像素的位置。这样可以将页面滚动到目标标题的位置。- 使用
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();
})
});
const { y } = useScroll(window)
: 这行代码使用了 Vue.js 3.0 引入的useScroll
函数,它返回一个包含滚动位置的对象y
。useScroll
函数用于监听窗口的滚动事件,并将滚动位置存储在对象y
中。watchThrottled(y, () => { ... }, { throttle: 200 })
: 这是一个使用自定义钩子函数watchThrottled
进行节流的代码块。它接受三个参数:滚动位置对象y
、回调函数和选项对象。回调函数会在滚动位置发生变化时被调用,选项对象中的throttle
属性指定了节流的时间间隔,单位为毫秒。在这里,节流时间设置为 200 毫秒,意味着回调函数每隔 200 毫秒才会被执行一次。- 回调函数中的代码遍历
titleList
数组,对于每个元素e
,通过查询选择器找到对应的标题元素heading
。然后,判断滚动位置是否超过了标题元素的顶部偏移量减去 50,如果超过,则将当前索引值赋给currentIndex
。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)