react
开发时遇到锚点目录需求,需要在页面滚动时高亮显示对应标题,点击时滚动到对应标题
而使用antd
的锚点组件时,遇到hash
路由不兼容问题,所以写了个简易的锚点目录跳转组件
功能点:
- 高亮显示当前标题
- 点击后滚动到对应标题
antd组件的兼容问题
在使用antd
组件时,虽然可以正常高亮显示对应标题,但在点击后会跳转到错误的页面
猜测是antd这个组件底层是使用a标签实现页面内滚动
tsx
<a href='#title1'/>
而这种滚动方式在遇到哈希路由时会有问题,会将哈希路由原本的#
号后面的路由覆盖,导致页面错误
下面开始造轮子,把这个组件实现出来,并且需要更换滚动的逻辑
如何使用
在讲组件实现之前,先确定一下如何使用,这样写起来才比较有方向
锚点组件称为Anchor
,使用这个组件,需要传入
scrollElement
:滚动的容器元素的idlist
:标题列表id
:标题idtitle
:标题的文本
tsx
<Anchor
scrollElement="#main"
list={[
{ id: "#title1", title: "标题1" },
{ id: "#title2", title: "标题2" },
{ id: "#title3", title: "标题3" },
{ id: "#title4", title: "标题4" },
]}
/>
<div id="main">
<h2 id="title1">标题1</h2>
<p>内容内容</p>
<h2 id="title2">标题2</h2>
<p>内容内容</p>
<h2 id="title3">标题3</h2>
<p>内容内容</p>
<h2 id="title4">标题4</h2>
<p>内容内容</p>
</div>
核心功能一:高亮显示当前标题
实现步骤
- 监听元素滚动
- 当前滚动了多少
- 比对标题在容器的相对位置
- 对应标题设置active
tsx
//1.监听元素滚动
let elem = document.querySelector(scrollElement);
elem.addEventListener("scroll", handleScroll);
function handleScroll(){
titleList.forEach((title, index) => {
const titleElem = document.querySelector(title.id);
//2. 当前滚动了多少
const scrollTop = elem.scrollTop
//3. 比对标题在容器的相对位置
if (scrollTop >= titleElem.offsetTop) {
//4. 对应标题设置为active
setActive(index);
}
});
}
//此处是简洁代码,下方有完整代码
原理
其中比较难的是第三步,可以看下方图解,滚动时scrollTop
是越来越大的,当大小超过了某个标题的offsetTop
时,就可以将这个标题设为active
了
图中scrollTop
大于标题1的offsetTop
,小于标题2,所以当前高亮的标题是标题1
加上对应css就可以高亮显示对应标题了
核心二:点击后滚动到对应标题
原理
这里滚动逻辑靠scroll
函数实现,scroll
可以将元素滚动到指定位置
通过scroll方法将容器元素滚动到标题所在的位置就能实现需求
tsx
container.scroll(titlePosition)
实现步骤
- 获取标题的相对位置和标题的高度
- 计算容器应该滚动到的位置
- 容器滚动
tsx
//将容器滚动到对应标题位置
//titleId:点击的标题的id
function scrollToElem(titleId) {
// 1.获取标题的相对位置和标题的高度
const linkElem = document.querySelector(titleId);
const top = linkElem.offsetTop;
const height = linkElem.offsetHeight;
//2.计算容器应该滚动到的位置
const position = top - height;
//3.容器滚动
const scrollElem = document.querySelector(scrollElement);
scrollElem.scroll({
top: position,
behavior: "smooth",
});
}
//此处是简洁代码,下方有完整代码
完整代码
使用组件
tsx
import React from "react";
import Anchor from "../components/Anchor";
export default function Home() {
return (
<div>
<Anchor
scrollElement="#main"
list={[
{ id: "#title1", title: "标题1" },
{ id: "#title2", title: "标题2" },
{ id: "#title3", title: "标题3" },
{ id: "#title4", title: "标题4" },
]}
/>
<div className="main" id="main">
<h2 id="title1">标题1</h2>
<p>内容内容</p>
<h2 id="title2">标题2</h2>
<p>内容内容</p>
<h2 id="title3">标题3</h2>
<p>内容内容</p>
<h2 id="title4">标题4</h2>
<p>内容内容</p>
</div>
</div>
);
}
组件jsx
tsx
import React, { useEffect, useState } from "react";
import _ from "lodash";
import "./index.css";
export default function PageAnchor({ list: l = [], scrollElement } = {}) {
//标题列表,默认高亮第一个标题
const [titleList, setList] = useState(
l.map((item, index) => {
if (!index) return { ...item, active: true };
return { ...item, active: false };
})
);
//监听元素滚动
useEffect(() => {
const handleScroll = _.throttle(function () {
titleList.forEach((title, index) => {
const titleElem = document.querySelector(title.id);
//2. 当前滚动了多少
const scrollTop = elem.scrollTop;
//3. 比对标题在容器的相对位置
if (scrollTop >= titleElem.offsetTop - 100) {
//4. 对应标题设置为active
setActive(index);
}
});
}, 200);
//1.监听元素滚动
let elem = document.querySelector(scrollElement);
elem.addEventListener("scroll", handleScroll);
}, []);
//设置某个标题高亮
function setActive(i) {
setList((list) =>
list.map((item, index) => {
if (index == i) return { ...item, active: true };
return { ...item, active: false };
})
);
}
//将容器滚动到对应标题位置
//titleId:点击的标题的id
function scrollToElem(titleId) {
// 1.获取标题的相对位置和标题的高度
const linkElem = document.querySelector(titleId);
const top = linkElem.offsetTop;
const height = linkElem.offsetHeight;
//2.计算容器应该滚动到的位置
const position = top - height;
//3.容器滚动
const scrollElem = document.querySelector(scrollElement);
scrollElem.scroll({
top: position,
behavior: "smooth",
});
}
return (
<div className="anchor">
{titleList.map((item) => (
<div
onClick={() => scrollToElem(item.id)}
className={`link ${item.active ? "active" : ""}`}
>
{item.title}
</div>
))}
</div>
);
}
组件css
css
.anchor {
position: fixed;
left: 10px;
top: 50px;
z-index: 3;
background-color: #e9eefa;
border-radius: 6px;
padding: 8px;
}
.anchor .link {
user-select: none;
cursor: pointer;
}
.anchor .active {
color: #3377ff;
}
其他细节
平缓滚动
通过阅读文档,可以知道scroll
函数可以设置behavior
来控制滚动是否平缓
smooth
表示平滑滚动并产生过渡效果auto
或缺省值会直接跳转到目标位置,没有过渡效果。
这里设置为平缓体验更好
tsx
scrollElem.scroll({
top: position,
behavior: "smooth",
});
节流函数
由于滚动会产生很多次事件,所以滚动事件回调最好用节流函数
tsx
//节流
const handleScroll = _.throttle(function () {
titleList.forEach((title, index) => {
const titleElem = document.querySelector(title.id);
const scrollTop = elem.scrollTop;
if (scrollTop >= titleElem.offsetTop - 100) {
setActive(index);
}
});
}, 200);
//1.监听元素滚动
elem.addEventListener("scroll", handleScroll);
待实现二级目录需求
大部分文章有二级或多级目录,而本组件只实现了一级目录
实现思路
- 在传入的list添加children属性
- 通过递归遍历子目录
- 子目录添加margin-left
tsx
list={[
{
id: "#title0",
title: "标题0"
},
{
id: "#title1",
title: "标题1",
children: [
{ id: "#title11", title: "标题11" },
{ id: "#title12", title: "标题12" },
],
},
]}