React项目中如何实现一个简单的锚点目录定位

news2024/11/27 5:28:11

在这里插入图片描述

小册

这是我整理的学习资料,非常系统和完善,欢迎一起学习

  • 现代JavaScript高级小册

  • 深入浅出Dart

  • 现代TypeScript高级小册

  • linwu的算法笔记📒

前言

锚点目录定位功能在长页面和文档类网站中非常常见,它可以让用户快速定位到页面中的某个章节

  • 如何在React中实现锚点定位和平滑滚动
  • 目录自动高亮的实现思路
  • 处理顶部导航遮挡锚点的解决方案
  • 服务端渲染下的实现方案
  • 性能优化策略

实现基本锚点定位

首先,我们需要实现页面内基本的锚点定位功能。对于锚点定位来说,主要涉及这两个部分:

  1. 设置锚点,为页面中的某个组件添加id属性
  2. 点击链接,跳转到指定锚点处

20210106205503299.gif
例如:

// 锚点组件
function AnchorComponent() {
  return <h2 id="anchor">This is anchor</h2> 
}

// 链接组件
function LinkComponent() {
  return (
    <a href="#anchor">Jump to Anchor</a> 
  )
}

当我们点击Jump to Anchor这个链接时,页面会平滑滚动到AnchorComponent所在的位置。

使用useScrollIntoView自定义hook

React中实现锚点定位,最简单的方式就是使用useScrollIntoView这个自定义hook。

import { useScrollIntoView } from 'react-use';

function App() {

  const anchorRef = useRef();  
  const scrollToAnchor = () => {
    useScrollIntoView(anchorRef);
  }

  return (
    <>
      <a href="#anchor" onClick={scrollToAnchor}>
        Jump to Anchor  
      </a>
      
      <h2 id="anchor" ref={anchorRef}>This is anchor</h2>
    </>
  )
}

useScrollIntoView接受一个ref对象,当调用这个hook函数时,会自动滚动页面,使得ref对象在可视区域内。

原生scrollIntoView方法

useScrollIntoView内部其实就是使用了原生的scrollIntoView方法,所以我们也可以直接调用:

function App() {

  const anchorRef = useRef();

  const scrollToAnchor = () => {
    anchorRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'start'
    })
  };

  return (
    <>  
      <a href="#anchor" onClick={scrollToAnchor}>Jump to Anchor</a>
      <h2 id="anchor" ref={anchorRef}>This is anchor</h2> 
    </>
  )

}

scrollIntoView可以让元素的父容器自动滚动,将这个元素滚动到可见区域。behavior:'smooth’可以启用平滑滚动效果。

锚点定位和目录联动

很多时候,我们会在页面中实现一个目录导航,可以快速定位到各个章节。此时就需要实现锚点定位和目录的联动效果:

  • 点击目录时,自动滚动到对应的章节
  • 滚动页面时,自动高亮正在浏览的章节

目录导航组件

目录导航本身是一个静态组件,我们通过props传入章节数据:

function Nav({ chapters }) {
  return (
    <ul className=" chapters">
      {chapters.map(chapter => (
        <li key={chapter.id}>
          <a href={'#' + chapter.id}>  
            {chapter.title}
          </a>
        </li>
      ))}
    </ul>
  )
}

锚点组件

然后在页面中的每一章使用Anchor组件包裹:

function Chapter({ chapter }) {
  return (
    <Anchor id={chapter.id}>  
      <h2>{chapter.title}</h2>
      {chapter.content}
    </Anchor>
  )
}

function Anchor({ children, id }) {
  return (
    <div id={id}>
      {children}  
    </div>
  )
}

这样通过id属性建立章节内容和目录链接之间的关联。

处理点击事件

当点击目录链接时,需要滚动到对应的章节位置:

function App() {

  //...

  const scrollToChapter = (chapterId) => {
    const chapterEl = document.getElementById(chapterId);
    chapterEl.scrollIntoView({ behavior: 'smooth' });
  }

  return (
    <>
      <Nav 
        chapters={chapters}
        onLinkClick={(chapterId) => scrollToChapter(chapterId)} 
      />
      
      {chapters.map(chapter => (
        <Chapter 
         key={chapter.id}
         chapter={chapter}
        />
      ))}
    </>
  )
}

给Nav组件传一个onLinkClick回调,当点击链接时,通过chapterId获取到元素,并滚动到可视区域,实现平滑跳转。

自动高亮

实现自动高亮也很简单,通过监听滚动事件,计算章节元素的偏移量,判断哪个章节在可视区域内,并更新active状态:

function App() {

  const [activeChapter, setActiveChapter] = useState();

  useEffect(() => {
    const handleScroll = () => {
      chapters.forEach(chapter => {
        const element = document.getElementById(chapter.id);
        // 获取元素在可视区域中的位置
        const rect = element.getBoundingClientRect();  
        // 判断是否在可视区域内 
        if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
          setActiveChapter(chapter.id);
        }
      })
    }

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    }
  }, []);

  return (
    <>
     <Nav
       chapters={chapters}
       activeChapter={activeChapter}
      />
    </>
  )

}

通过getBoundingClientRect可以得到元素相对于视窗的位置信息,根据位置判断是否在可见区域内,如果是就更新activeChapter状态,从而触发目录的高亮效果。

问题解析

遮挡问题

有时锚点会被固定的Header遮挡,此时滚动会定位到元素上方,用户看不到锚点对应的内容。

常见的解决方案是:

  1. 设置锚点元素margin-top
#anchor {
  margin-top: 80px; /* header高度 */
}

直接设置一个和Header高度相同的margin,来防止遮挡。

  1. 在滚动方法中加入offset
// scroll offset
const scrollOffset = -80; 

chapterEl.scrollIntoView({
  offsetTop: scrollOffset
})

给scrollIntoView传入一个顶部偏移量,这样也可以跳过Header的遮挡。

响应式问题

在响应式场景下,目录的遮挡问题会更复杂。我们需要区分不同断点下,计算匹配的offset。

可以通过MatchMedia Hook获取当前的断点:

import { useMediaQuery } from 'react-responsive';

function App() {

  const isMobile = useMediaQuery({ maxWidth: 767 });
  const isTablet = useMediaQuery({ minWidth: 768, maxWidth: 1023 });
  const isDesktop = useMediaQuery({ minWidth: 1024 });
  
  let scrollOffset = 0;

  if (isMobile) {
    scrollOffset = 46; 
  } else if (isTablet) {  
    scrollOffset = 60;
  } else if (isDesktop) {
    scrollOffset = 80;
  }

  const scrollToChapter = (chapterId) => {
    const chapterEl = document.getElementById(chapterId);

    chapterEl.scrollIntoView({
      offsetTop: scrollOffset  
    })
  }

  //...

}

根据不同断点,动态计算滚动偏移量,这样可以适配所有情况。

性能优化

使用节流

滚动事件会高频触发,直接在滚动回调中计算章节位置会造成性能问题。

我们可以使用Lodash的throttle函数进行节流:

import throttle from 'lodash.throttle';

const handleScroll = throttle(() => {
  // 计算章节位置
}, 100);

这样可以限制滚动事件最多每100ms触发一次。

IntersectionObserver

使用IntersectionObserver提供的异步回调,只在章节进入或者离开可视区域时才执行位置计算:

import { useRef, useEffect } from 'react';

function App() {

  const chaptersRef = useRef({});

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        // 章节进入或者离开可视区域时更新
      }
    );

    chapters.forEach(chapter => {
      observer.observe(
        document.getElementById(chapter.id)  
      );
    })

  }, []);

} 

这种懒加载式的方式可以大幅减少无效的位置计算。

SSR支持

在Next.js等SSR场景下,客户端脚本会延后加载,页面初次渲染时目录联动会失效。

getInitialProps注水

可以在getInitialProps中提前计算目录数据,注入到页面中:

Home.getInitialProps = async () => {

  const chapters = await fetchChapters();

  const mappedChapters = chapters.map(chapter => {
    return {
      ...chapter,
      highlighted: isChapterHighlighted(chapter) 
    }
  });

  return {
    chapters: mappedChapters
  };

};

hydrate处理

客户端脚本加载后,需要调用ReactDOM.hydrate而不是render方法,进行数据的补充填充,避免目录状态丢失。

import { useEffect } from 'react';

function App({ chapters }) {

  useEffect(() => {
    ReactDOM.hydrate(
      <App chapters={chapters} />,  
      document.getElementById('root')
    );
  }, []);

}

服务端渲染的实现方案

image.png

在使用了服务端渲染(SSR)的框架如Next.js等情况下,实现锚点定位和目录联动也会有一些不同。

主要区别在于:

  • 服务端和客户端环境不统一
  • 脚本加载时间差

这会导致一些状态错位的问题。

问题复现

假设我们有下面的目录和内容结构:

function Nav({ chapters }) {
  return (
    <ul>
      {chapters.map(ch => (
        <li>
          <a href={'#' + ch.id}>{ch.title}</a>
        </li>
      ))}
    </ul>
  )
}

function Chapter({ chapter }) {

  const ref = useRef();

  // 占位组件
  return <div ref={ref}>{chapter.content}</div> 
}

function App() {

  const chapters = [
    { id: 'chapter-1', title: 'Chapter 1' },
    { id: 'chapter-2', title: 'Chapter 2' },
  ];

  return (
    <>
      <Nav chapters={chapters} />

      <Chapter chapter={chapters[0]} />
      <Chapter chapter={chapters[1]} />
    </>
  )

}

非SSR环境下,点击链接和滚动都可以正常工作。

但是在Next.js的SSR环境下就会有问题:

点击目录链接时,页面不会滚动。

这是因为在服务端,我们无法获取组件的ref,所以锚点元素不存在,自然无法定位。

滚动页面时,目录高亮也失效。

服务端渲染的静态HTML中,并没有绑定滚动事件,所以无法自动高亮。

预取数据

首先,我们需要解决点击目录链接的问题。

既然服务端无法获取组件ref,那就需要在客户端去获取元素位置。

这里有两个方法:

  1. 组件挂载后主动缓存元素位置
// Chapter组件

useEffect(() => {
  // 缓存位置数据
  cacheElementPosition(chapter.id, ref.current); 
}, []);

// Utils

const elementPositions = {};

function cacheElementPosition(id, element) {
  const rect = element.getBoundingClientRect();

  elementPositions[id] = {
    left: rect.left,
    top: rect.top,
  }
}
  1. 点击时实时获取元素位置
// handle link click

const scrollToChapter = (chapterId) => {

  const element = document.getElementById(chapterId);
  const rect = element.getBoundingClientRect();

  window.scrollTo({
    top: rect.top,
    behavior: 'smooth'
  })

}

无论哪种方法,都需要在组件挂载后获取元素的位置信息。

这样我们就可以在点击目录链接时,正确滚动到对应的章节位置了。

数据注水

但是点击目录只解决了一半问题,滚动高亮还需要解决。

这里就需要用到数据注水的技术。

简单来说就是:

  • 在服务端渲染时,读取路由参数,提前计算高亮状态
  • 将高亮数据注入到响应中
  • 客户端拿到注水的数据后渲染,不会出现高亮错位

实现步骤:

1.服务端获取参数和数据

// 在getServerSideProps中

export async function getServerSideProps(context) {
  
  const { hashtag } = context.query;

  const chapters = await fetchChapters();

  const highlightedChapter = chapters.find(ch => ch.id === hashtag);

  return {
    props: {
      chapters,
      highlightedChapter  
    }
  }

}

2.客户端读取props

function Nav({ chapters, highlightedChapter }) {

  return (
    <ul>
      {chapters.map(ch => (
        <li className={ch.id === highlightedChapter?.id ? 'highlighted' : ''}>
        </li>
      ))}
    </

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1042435.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Pridwen: Universally Hardening SGX Programs via Load-Time Synthesis【ATC`22】

目录 摘要引言性能贡献 背景英特尔SGXSGX的异常SGX侧通道WebAssembly&#xff08;Wasm&#xff09;Wasm中的内存安全PRIDWEN和Wasm 概述场景威胁模型目标适应性证明性可扩展性 架构 标题&#xff1a;Pridwen: Universally Hardening SGX Programs via Load-Time Synthesis 作者…

系统学习Mysql

1.select语句 关键字执行顺序&#xff1a; 1.from 2.where 3.group by 4.select 5.having 6.order by 7.limit SQL 语句执行顺序如下&#xff1a; FROM: 指定要查询的表或子查询&#xff0c;可以包含 JOIN、WHERE 子句过滤等。 WHERE: 对 FROM 子句指定的表或子查询进行限制和…

OpenCV之直线曲线拟合

直线拟合fitLine void fitLine( InputArray points, OutputArray line, int distType,double param, double reps, double aeps ); points:二维点的数组或vector line:输出直线,Vec4f (2d)或Vec6f (3d)的vector distType:距离类型 param:距离参数 reps:径向的精度参数 a…

Tomcat Takeover

靶场介绍 蓝队靶场练习之Tomcat Takeover 场景介绍 我们的 SOC 团队在公司内部网的一台 Web 服务器上检测到可疑活动。为了更深入地了解情况&#xff0c;该团队捕获了网络流量进行分析。此pcap文件可能包含一系列恶意活动&#xff0c;这些活动已导致Apache Tomcat Web服务器…

Cairo介绍及源码构建安装(1)

一、简介 Cairo是一个支持多个输出设备的2D图形库。目前支持的输出目标包括X Window System、win32、图像缓冲区、PostScript、PDF和SVG。实验后端包括OpenGL、Quartz和XCB文件输出。Cairo旨在在所有输出媒体上产生一致的输出&#xff0c;同时利用可用的显示硬件加速&#xff…

基于 ESP32-C2 的 Wi-Fi/BLE 和 LoRa 极低成本无线组网方案

在物联网高速发展的今天&#xff0c;Wi-Fi 和 LoRa 作为近距离和超远距离通信技术&#xff0c;均有其各自的典型应用场景&#xff0c;在各自应用领域都取得了卓越的成就。Wi-Fi 和 LoRa 技术各具优势&#xff0c;在很多场景需求中&#xff0c;如果将两者结合&#xff0c;一方面…

自动化测试如何落地,一篇搞定

前言 前段时间面试了某零售电商企业的测试经理岗位&#xff0c;面试官当时提了这样一个问题&#xff1a;我们这边测试团队开展自动化测试工作将近一年了&#xff0c;但目前还未看到明显的对测试过程或者质量的改善效果。如果是你&#xff0c;你会如何做&#xff1f; 自动化测…

计算机算法分析与设计(4)---矩阵连乘问题(含C++代码)

文章目录 一、概述1.1 矩阵乘法1.2 穷举法1.3 动态规划 二、代码编写2.1 例题分析2.2 代码 一、概述 1.1 矩阵乘法 1. 矩阵相乘&#xff0c;前一个矩阵的列数需等于后一个矩阵的行数。相乘得到的新矩阵&#xff0c;其行数由前一个矩阵决定&#xff0c;其列数由后一个矩阵决定。…

可视化工具Datart踩(避)坑指南(4)——丢失的精度

作为目前国内开源版本最好用的可视化工具&#xff0c;Datart无疑是低成本高效率可供二开的可视化神兵利器。当然&#xff0c;免费的必然要付出一些踩坑的代价。本篇我们来讲一讲可视化工具Datart踩&#xff08;避&#xff09;坑指南&#xff08;4&#xff09;之丢失的精度。 版…

python -文件相关操作

文章目录 前言python -文件相关操作1. 读取文件1.1. 读取整个文件内容1.2. 读取文件的一行内容1.3. 将文件的内容按行存储到一个列表中 2. 写入文件3. 删除文件4. 追加文件5. 遍历文件5.1. 使用 os 模块 遍历文件5.2. # 使用 glob 模块 遍历文件5.3. 使用os.listdir() 函数遍历…

C语言——运算符

C用运算符表示算术运算。 C没有指数运算符&#xff0c;不过&#xff0c;C的标准数学库提供了一个pow()函数用于指数运算。 基本运算符 赋值运算符&#xff1a; 变量名变量值 从右到左 左值和变量名的区别&#xff1a; 变量名是一个标识符的名称&#xff0c;左值是一个可变…

SpringBoot+MinIO8.0开箱即用的启动器

一、代码拉取及安装 1.码云地址 https://gitee.com/qiangesoft/rdp-starter/tree/master/rdp-starter-minio 2.本地安装 代码接入 1.引入依赖 <dependency><groupId>com.qiangesoft.rdp</groupId><artifactId>rdp-starter-minio</artifactId&g…

国密国际SSL双证书解决方案,满足企事业单位国产国密SSL证书要求

近年来&#xff0c;为了摆脱对国外技术和产品的依赖&#xff0c;建设安全的网络环境&#xff0c;以及加强我国对网络信息的安全可控能力&#xff0c;我国推出了国密算法。同时&#xff0c;为保护网络通信信息安全&#xff0c;更高级别的安全加密数字证书—国密SSL证书应运而生。…

如何办一份有价值的企业内刊/报纸?向《华为人》学习就够了

前两天有一个朋友联系华研荟&#xff0c;说他是今年大学毕业加入了一个中型公司&#xff0c;他学的是企业管理&#xff0c;在公司人力资源部门工作。上周老板说公司要办一份自己的内刊&#xff0c;这个工作由人力资源部负责&#xff0c;而人力资源经理就把这个活交给她了。 她…

800G时代来临,千兆光模块万兆光模块还有用吗?

随着科技的不断进步&#xff0c;网络传输速度的需求也越来越高&#xff0c;特别是在云计算、人工智能、物联网等领域&#xff0c;对网络传输速度的要求越来越高。近年来&#xff0c;千兆光模块和万兆光模块已经成为了网络传输的主要手段&#xff0c;但随着800G时代的到来&#…

如何正确使用MySQL的索引呢?

前言: 📕作者简介:热爱编程的小七,致力于C、Java、Python等多编程语言,热爱编程和长板的运动少年! 📘相关专栏Java基础语法,JavaEE初阶,数据库,数据结构和算法系列等,大家有兴趣的可以看一看。 😇😇😇有兴趣的话关注博主一起学习,一起进步吧! 一、索引使用…

掌握这些技巧,轻松批量压缩视频大小

如果您需要批量压缩视频文件的大小&#xff0c;可以借助固乔科技官网提供的固乔剪辑助手软件。下面我们将详细介绍如何使用该软件实现这一目标。 1. 进入固乔科技官网&#xff0c;下载并安装固乔剪辑助手软件。这款软件支持Windows和Mac操作系统&#xff0c;可以方便地在官网上…

Android布局转图片Bitmap

最近再写一个PDA蓝牙连接打印机打印标签&#xff08;包含商品名、原价、现价、尺寸等等...&#xff09;这就需要自己布局一个view样式&#xff0c;转换成bitmap&#xff0c;然后用打印机打印出来。 先看图&#xff1a; 下面事layout布局转bitmap的方法。 网上很多介绍View转b…

雨课堂 运动与健康 网课参考资料

整理于网络&#xff1a;仅用于学习交流讨论&#xff0c;侵删 参考文档&#xff1a;https://www.doc88.com/p-99629779008847.html 参考视频&#xff1a; 运动与健康&#xff08;2021年秋网课答案 69题版本&#xff09;_哔哩哔哩 | https://www.bilibili.com/video/av210112837/…

C语言自定义类型(下)

大家好&#xff0c;我们今天来学习C语言自定义类型剩下的内容。 目录 1.枚举 2.联合 1.枚举类型 枚举顾名思义就是一一列举。 把可能的取值一一列举。 一周的星期一到星期日是有限的7天&#xff0c;可以一一列举。 性别有&#xff1a;男、女、保密&#xff0c;也可以一一列…