实现文章目录索引
模仿掘金实现文章的目录和索引功能
需要实现的功能:
- 根据文章列表生成目录结构
- 点击目录索引,页面自动滚动到对应区域
- 页面滚动时,目录中对应索引自动高亮
根据文章列表生成目录结构这个比较简单,就是对文章列表进行一个遍历,展示段落标题即可。
我们主要看下后两个功能如何实现。
1. 点击目录索引,页面自动滚动到对应区域
我们在渲染文章列表时,针对每一个段落的 dom 元素,都会设置一个唯一的 id 进行标识
当我们点击目录标题时,也可以拿到对应段落的 id,然后获取这个 id 对应的 dom 元素,取到其中的 offsetTop 属性,然后使用 Window.scrollTo(),即可滚动到指定位置
<li
key={cIndex}
className={linkClassName(id)}
onClick={() => {
setActiveCategory(id)
// 1. 获取 id 对应 dom 元素的 offsetTop
const ele: HTMLElement = document.querySelector(id)
const { offsetTop } = ele
// 2. 根据 offsetTop 滚动到指定位置
window.scrollTo({
top: offsetTop,
})
}}>
{category.category_name}
</li>
2. 页面滚动时,目录中对应索引自动高亮
实现这个功能,首先就是需要监听滚动元素的 GlobalEventHandlers.onscroll 事件,滚动事件触发时,我们获取到 event 对象,此时需要获取到 event.target.documentElement.scrollTop ,这个属性就表示我们当前滚动区域到顶部的距离,我们就可以根据这个值来判断当前应该高亮哪个段落的标题。
实现目录索引功能时我们知道每个段落的 dom 元素都有一个 offsetTop 属性,我们可以提前获取每个元素的 offsetTop 属性,用一个对象数组维护 id 和 offsetTop 的映射关系,当滚动事件的 event.target.documentElement.scrollTop 属性某个段落对应的 offsetTop 相等时,就高亮对应段落的目录标题即可。但是滚动事件的触发比较频繁,可能无法刚好与某个 offsetTop 相等,所以设置一个区间更合理。我们取当前段落的 offsetTop 到下一个段落的 offsetTop 为一个区间,当 scrollTop 到达这个区间时,就高亮第一个段落的目录标题。
// 1. 获取 id 对应的 offsetTop 区间
useEffect(() => {
categoryEles.current = [];
categoryList?.forEach((category, index: number) => {
const id = `#category-${category.category_id}`;
const el: HtmlElement = document.querySelector(id);
let categoryTopRange: CategoryTopRange = {
id,
start: 0,
end: 0,
};
if (index === categoryList.length - 1) {
categoryTopRange = {
id,
start: el.offsetTop,
end: document.body.offsetHeight, // 最后一个段落的区间 end 就是 body 的高度
};
} else {
const nextCategory: VolumeCategory = categoryList[index + 1];
const nextEl: any = document.querySelector(
`#category-${nextCategory.category_id}`
);
categoryTopRange = {
id,
start: el.offsetTop,
end: nextEl.offsetTop,
};
}
categoryEles.current.push(categoryTopRange);
});
}, [categoryList]);
// 2. 监听滚动事件,当滚动到某个区间时,激活对应段落状态
useEffect(() => {
const body = document.getElementsByTagName('body')[0];
console.log(body);
body.onscroll = (e: any) => {
if (!ticking.current) {
window.requestAnimationFrame(function () {
const top = e.target.documentElement.scrollTop || 0;
console.log({ top });
const category: CategoryTopRange | undefined =
categoryEles.current.find(
(cate: CategoryTopRange) => cate.start <= top && cate.end > top
);
if (category) {
setActiveCategory((category as CategoryTopRange).id);
}
ticking.current = false;
});
ticking.current = true;
}
};
}, [categoryEles]);
3. 滚动到顶部
这个比较简单
<div
onClick={() => {
// 滚动到顶部
window.scrollTo({
top: 0,
})
}}
>
<ToTop />
</div>