markdown与锚点的交互使用是现在博客网站的标配。例如CSDN、简书、掘金,都有类似的功能。最近在开发博客网站时也遇到了同样的需求。本文详细总结了markdown与锚点交互相关的问题解决方法,如果你的博客网站也有类似的需求,希望这篇文章可以帮助到你。
一、功能需求与效果
1. 实现功能
- 根据markdown的h标题,自动生成文章大纲目录
- 点击锚点时,页面滑动到对应的位置,且锚点高亮
- 当页面滑动时,滑动到指定位置时,对应的锚点高亮显示
2. 效果示例
这种效果在csdn、简书、掘金上都有类型的功能。包括本人的博客网站也有,右侧为markdown锚点。
二、获取锚点列表
- 使用markdown编辑器地址:https://github.com/code-farmer-i/vue-markdown-editor。其他markdown也是采用同样的思路实现。
1. 思路分析
查看网页源代码分析可知,markdown所有标题均为h1-h6的标签。通过js的标签选择器,选中这些标签,组成一个列表。列表中除了要包含标题的名称外,还有标题距离顶部的距离(当前锚点高亮使用)、标题的层级(生成目录时,不同层级左侧缩进距离不同)、标题的id(点击标题滚动时,跳转到指定id的位置)
2. 示例代码
<template>
<div>
<div
v-for="anchor in titleList"
:style="{ padding: `10px 0 10px ${anchor.indent * 20}px` }"
>
<a style="cursor: pointer">{{ anchor.title }}</a>
</div>
<v-md-preview :text="text" ref="editor" />
</div>
</template>
<script setup>
import {ref, onMounted, nextTick} from 'vue';
import {getArticleDetail} from "@/api/blog";
// markdown-文章内容
const article = ref()
// markdown-对象
const editor = ref(null)
// markdown-文章标题列表
const titleList = ref([])
// markdown-获取内容
async function articleData(DetailID) {
// axios获取内容
article.value = await getArticleDetail(DetailID)
}
// markdown-生成标题
async function getTitle() {
await nextTick()
// 使用js选择器,获取对应的h标签,组合成列表
const anchors = editor.value.querySelectorAll(
'.v-md-editor-preview h1,h2,h3,h4,h5,h6'
)
// 删除标题头尾的空格
const titles = Array.from(anchors).filter((title) => !!title.innerText.trim());
// 当文章h标签为空时,直接返回
if (!titles.length) {
titleList.value = [];
return;
}
// 从h标签属性中,提取相关信息
const hTags = Array.from(new Set(titles.map((title) => title.tagName))).sort();
titleList.value = titles.map((el) => ({
title: el.innerText, // 标题内容
lineIndex: el.getAttribute('data-v-md-line'), // 标签line id
indent: hTags.indexOf(el.tagName), // 标签层级
height: el.offsetTop, // 标签距离顶部距离
}));
}
onMounted(async () => {
// 获取第一篇文章内容
await articleData(1)
// 生成文章标题列表
await getTitle()
})
</script>
<style lang="scss">
………
</style>
- 其中h标签的详细属性信息可以通过控制台打印anchors查看
3. 效果演示
- 通过vue devtools可以查看到提取的标签列表如下所示
- 页面生成的大纲内容如下所示
三、点击锚点滚动
1. 思路分析
点击锚点滚动实现起来较为简单,基本思路是定义一个变量存放当前高亮的标题列表的id,通过三元表达式添加当前标签高亮的class。页面目录元素添加点击事件,传入标签对象,js查找对应的lineIndex,实现页面滚动跳转
2. 示例代码
<template>
<div>
<div
v-for="anchor in titleList"
:style="{ padding: `10px 0 10px ${anchor.indent * 20}px` }"
@click="rollTo(anchor,index)" :class="index===heightTitle?'title-active':''"
>
<a style="cursor: pointer">{{ anchor.title }}</a>
</div>
<v-md-preview :text="text" ref="editor" />
</div>
</template>
<script setup>
import {ref, onMounted, nextTick} from 'vue';
import {getArticleDetail} from "@/api/blog";
// markdown-文章内容
const article = ref()
// markdown-对象
const editor = ref(null)
// markdown-文章标题列表
const titleList = ref([])
// markdown-获取内容
async function articleData(DetailID) {
……
}
// markdown-生成标题
async function getTitle() {
……
}
// markdown-当前高亮的标题index
const heightTitle = ref(0)
// markdown-标题跳转
const rollTo = (anchor, index) => {
// 获取要跳转的标签的lineIndex
const {lineIndex} = anchor;
// 查找lineIndex对应的元素对象
const heading = editor.value.querySelector(
`.v-md-editor-preview [data-v-md-line="${lineIndex}"]`
);
// 页面跳转
if (heading) {
heading.scrollIntoView({behavior: "smooth", block: "start"})
}
// 修改当前高亮的标题
heightTitle.value = index
}
onMounted(async () => {
// 获取第一篇文章内容
await articleData(1)
// 生成文章标题列表
await getTitle()
})
</script>
<style lang="scss">
.title-active {
background-color: $color-background-input;
color: $color-text-primary;
border-left: 2px solid $color-primary;
}
</style>
3. 效果演示
- 点击目录标题,跳转至对应位置
- 控制台打印lineIndex对应的元素对象
四、当前锚点高亮
1. 思路分析
获取markdown所有标签对应的高度。然后监听页面的滚动事件。当每次页面滚动时,查找页面滚动值和标签高度最接近的那个标签对应的index。通过三元表达式添加当前标签高亮的class。
2. 示例代码
<template>
<div>
<div
v-for="anchor in titleList"
:style="{ padding: `10px 0 10px ${anchor.indent * 20}px` }"
@click="rollTo(anchor,index)" :class="index===heightTitle?'title-active':''"
>
<a style="cursor: pointer">{{ anchor.title }}</a>
</div>
<v-md-preview :text="text" ref="editor" />
</div>
</template>
<script setup>
import {ref, onMounted, nextTick, onBeforeUnmount} from 'vue';
import {getArticleDetail} from "@/api/blog";
// markdown-文章内容
const article = ref()
// markdown-对象
const editor = ref(null)
// markdown-文章标题列表
const titleList = ref([])
// markdown-获取内容
async function articleData(DetailID) {
……
}
// markdown-生成标题
async function getTitle() {
……
}
// markdown-当前高亮的标题index
const heightTitle = ref(0)
// markdown-标题跳转
const rollTo = (anchor, index) => {
……
}
// markdown-页面滚动。
const scroll = () => {
// 监听屏幕滚动时防抖(在规定的时间内触发的事件,只执行最后一次,降低性能开销)
let timeOut = null; // 初始化空定时器
return () => {
clearTimeout(timeOut) // 频繁操作,一直清空先前的定时器
timeOut = setTimeout(() => { // 只执行最后一次事件
let scrollTop = window.pageYOffset
// console.log(window.pageYOffset)
const absList = [] // 各个h标签与当前距离绝对值
titleList.value.forEach((item) => {
absList.push(Math.abs(item.height - scrollTop))
})
// 屏幕滚动距离与标题高度最近的index高亮
heightTitle.value = absList.indexOf(Math.min.apply(null, absList))
}, 500)
}
}
onMounted(async () => {
……
// 监听页面滚动事件
window.addEventListener('scroll', scroll())
})
onBeforeUnmount(() => {
// 离开该页面时移除这个监听的事件
window.removeEventListener('scroll', scroll())
})
</script>
<style lang="scss">
.title-active {
background-color: $color-background-input;
color: $color-text-primary;
border-left: 2px solid $color-primary;
}
</style>
3. 效果演示
- 控制台打印scrollTop和heightTitle查看
4. 完整代码
<template>
<div>
<div
v-for="anchor in titleList"
:style="{ padding: `10px 0 10px ${anchor.indent * 20}px` }"
@click="rollTo(anchor,index)" :class="index===heightTitle?'title-active':''"
>
<a style="cursor: pointer">{{ anchor.title }}</a>
</div>
<v-md-preview :text="text" ref="editor" />
</div>
</template>
<script setup>
import {ref, onMounted, nextTick, onBeforeUnmount} from 'vue';
import {getArticleDetail} from "@/api/blog";
// markdown-文章内容
const article = ref()
// markdown-对象
const editor = ref(null)
// markdown-文章标题列表
const titleList = ref([])
// markdown-获取内容
async function articleData(DetailID) {
// axios获取内容
article.value = await getArticleDetail(DetailID)
}
// markdown-生成标题
async function getTitle() {
await nextTick()
// 使用js选择器,获取对应的h标签,组合成列表
const anchors = editor.value.querySelectorAll(
'.v-md-editor-preview h1,h2,h3,h4,h5,h6'
)
// 删除标题头尾的空格
const titles = Array.from(anchors).filter((title) => !!title.innerText.trim());
// 当文章h标签为空时,直接返回
if (!titles.length) {
titleList.value = [];
return;
}
// 从h标签属性中,提取相关信息
const hTags = Array.from(new Set(titles.map((title) => title.tagName))).sort();
titleList.value = titles.map((el) => ({
title: el.innerText, // 标题内容
lineIndex: el.getAttribute('data-v-md-line'), // 标签line id
indent: hTags.indexOf(el.tagName), // 标签层级
height: el.offsetTop, // 标签距离顶部距离
}));
}
// markdown-当前高亮的标题index
const heightTitle = ref(0)
// markdown-标题跳转
const rollTo = (anchor, index) => {
// 获取要跳转的标签的lineIndex
const {lineIndex} = anchor;
// 查找lineIndex对应的元素对象
const heading = editor.value.querySelector(
`.v-md-editor-preview [data-v-md-line="${lineIndex}"]`
);
// 页面跳转
if (heading) {
heading.scrollIntoView({behavior: "smooth", block: "start"})
}
// 修改当前高亮的标题
heightTitle.value = index
}
// markdown-页面滚动。
const scroll = () => {
// 监听屏幕滚动时防抖(在规定的时间内触发的事件,只执行最后一次,降低性能开销)
let timeOut = null; // 初始化空定时器
return () => {
clearTimeout(timeOut) // 频繁操作,一直清空先前的定时器
timeOut = setTimeout(() => { // 只执行最后一次事件
let scrollTop = window.pageYOffset
// console.log(window.pageYOffset)
const absList = [] // 各个h标签与当前距离绝对值
titleList.value.forEach((item) => {
absList.push(Math.abs(item.height - scrollTop))
})
// 屏幕滚动距离与标题高度最近的index高亮
heightTitle.value = absList.indexOf(Math.min.apply(null, absList))
}, 500)
}
}
onMounted(async () => {
// 获取第一篇文章内容
await articleData(1)
// 生成文章标题列表
await getTitle()
// 监听页面滚动事件
window.addEventListener('scroll', scroll())
})
onBeforeUnmount(() => {
// 离开该页面时移除这个监听的事件
window.removeEventListener('scroll', scroll())
})
</script>
<style lang="scss">
.title-active {
background-color: $color-background-input;
color: $color-text-primary;
border-left: 2px solid $color-primary;
}
</style>
参考链接
https://www.jianshu.com/p/e07a8e4efb7d
http://ckang1229.gitee.io/vue-markdown-editor/zh/senior/anchor.html
https://blog.csdn.net/weixin_41192489/article/details/111284951
查看更多
微信公众号
微信公众号同步更新,欢迎关注微信公众号第一时间获取最近文章。
博客网站
崔亮的博客-专注devops自动化运维,传播优秀it运维技术文章。更多原创运维开发相关文章,欢迎访问https://www.cuiliangblog.cn