前言
前几天把博客用 Astro 重写了一遍,换了一套样式,干脆再顺手把目录也做上吧。本着怎么省事怎么来的原则,我直接从网上抄了一段 gist,但是发现它并不能很好地识别当前阅读的章节,于是我在它的基础上使用 IntersectionObserver 重写了一套识别方案。
本文将实现在页面滚动时自动高亮当前阅读章节。
前置准备
为了实现本文的目标,需要如下准备
- 已经在 html 中渲染出文章内容,其中
<hx>
标题元素需要有 id
属性。 - 已经根据文章内容生成好目录 DOM 结构,并且在页面滚动时能一直看到目录。
- 目录结构如下
如何找出正在阅读的章节
我们先定义一下什么是正在阅读的章节:
在当前页面顶部 50% 的地方作一条横线,从这条横线向上找,找到的第一个标题就是当前正在阅读的章节。
IntersectionObserver
既然我们的目标是在页面滚动时自动识别出正在阅读的章节,第一想法当然是通过 onscroll
事件来监听页面滚动,滚动时去获取每个标题的位置,计算出正在阅读的章节。但是 onscroll
事件会在滚动时持续触发,会在一定程度上影响性能。取而代之的方案是使用 IntersectionObserver,它只会在元素进出检测区域时触发,虽然它的兼容性不如 onscroll
方案,但是在桌面端也有 97% 的支持率,足够我的博客使用了。
使用 IntersectionObserver 大概分为 3 步:
- 调用
const ob = new IntersectionObserver(callback, opts)
创建一个实例并指定检测区域。 - 调用
ob.observe(el)
监视 el
元素进出检测区域。 - (可选)调用
ob.disconnect()
来结束监视。
元素被监视后,当它进入检测区域后,callback
将被调用,入参是一个数组,包含了所有当前时刻进入或离开检测区域的元素。一般滚动时这个数组只会有一个元素,因为滚动时一般只会有一个元素同时进入检测区域;但如果点击浏览器右侧的滚动条直接跳转到文章的某个区域,那么会出现数组中包含多个元素的情况。
opts
则由这些参数组成
root
一个作为检测区域的元素,设为 null
则会将浏览器视口作为检测区域。rootMargin
检测区域向外扩展的大小,例如 0px
表示检测区域不向外扩展,注意不能省略 px
0px 10px -20% 30px
表示检测区域 - 顶部边界不扩展
- 右边界向外扩展 10px
- 底部边界向内缩回 20% 的高度
- 左边界向外扩展 30px
threshold
一个数组,表示被检测元素进入检测区域达到多少百分比时才触发 callback
,例如 [0]
表示被检测元素一进入检测区域就触发,哪怕只进入了 1px[0.5]
表示被检测元素的一半进入检测区域就触发[1]
表示被检测元素需要完全进入检测区域才会触发[0, 1]
表示被检测元素一进入检测区就会触发,并且在元素完全进入检测区域后再触发一次
找出检测区域内的所有标题
因为 IntersectionObserver 触发时只会传入当前进入或离开检测区域的元素,所以我们还需要额外写点代码记录一下当前检测区域内都有哪些标题。
找出正在阅读章节的标题
现在我们拿到了所有处于检测区内的所有标题,接下来就可以找出正在阅读章节的标题了,callback
被触发时会有这三种情况:
- 当前有标题在检测区域内,最靠底部的一个标题是当前阅读章节的标题
- 当前没有标题在检测区域内,说明传入的元素都是离开检测区的
- 如果这些元素里有处于检测区域上方的,那这些处于检测区域上方的标题中最下方的一个就是当前阅读章节的标题
- 如果这些元素全处于检测区域下方,则这些元素中最上方的那个标题之前的标题是当前阅读章节的标题
于是我们得到了这样的代码
给目录加样式
在拿到正在阅读章节的标题后,我们就可以查找该标题对应的目录项并给它加上一个 css class,于是就实现了随滚动高亮目录中当前阅读章节的功能。
完整代码
参考