兼容hash路由的页面内锚点跳转组件

react开发时遇到锚点目录需求,需要在页面滚动时高亮显示对应标题,点击时滚动到对应标题

而使用antd的锚点组件时,遇到hash路由不兼容问题,所以写了个简易的锚点目录跳转组件

功能点:

  1. 高亮显示当前标题
  2. 点击后滚动到对应标题

antd组件的兼容问题

在使用antd组件时,虽然可以正常高亮显示对应标题,但在点击后会跳转到错误的页面

猜测是antd这个组件底层是使用a标签实现页面内滚动

tsx 复制代码
<a href='#title1'/>

而这种滚动方式在遇到哈希路由时会有问题,会将哈希路由原本的#号后面的路由覆盖,导致页面错误

下面开始造轮子,把这个组件实现出来,并且需要更换滚动的逻辑

如何使用

在讲组件实现之前,先确定一下如何使用,这样写起来才比较有方向

锚点组件称为Anchor,使用这个组件,需要传入

  • scrollElement:滚动的容器元素的id
  • list:标题列表
    • id:标题id
    • title:标题的文本
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>

核心功能一:高亮显示当前标题

实现步骤

  1. 监听元素滚动
  2. 当前滚动了多少
  3. 比对标题在容器的相对位置
  4. 对应标题设置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可以将元素滚动到指定位置

Element.scroll()

通过scroll方法将容器元素滚动到标题所在的位置就能实现需求

tsx 复制代码
container.scroll(titlePosition)

实现步骤

  1. 获取标题的相对位置和标题的高度
  2. 计算容器应该滚动到的位置
  3. 容器滚动
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" },
          ],
      },
      ]}
相关推荐
有梦想的刺儿14 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具35 分钟前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161771 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo2 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v2 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫2 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web