小册

这是我整顿的学习材料,十分零碎和欠缺,欢送一起学习
  • 古代JavaScript高级小册
  • 深入浅出Dart
  • 古代TypeScript高级小册
  • linwu的算法笔记

前言

锚点目录定位性能在长页面和文档类网站中十分常见,它能够让用户疾速定位到页面中的某个章节

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

实现根本锚点定位

首先,咱们须要实现页面内根本的锚点定位性能。对于锚点定位来说,次要波及这两个局部:
  1. 设置锚点,为页面中的某个组件增加id属性
  2. 点击链接,跳转到指定锚点处


例如:

// 锚点组件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 offsetconst 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')    );  }, []);}

服务端渲染的实现计划

在应用了服务端渲染(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); }, []);// Utilsconst elementPositions = {};function cacheElementPosition(id, element) {  const rect = element.getBoundingClientRect();  elementPositions[id] = {    left: rect.left,    top: rect.top,  }}
  1. 点击时实时获取元素地位
// handle link clickconst 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>      ))}    </