在React項目中實現(xiàn)一個簡單的錨點目錄定位
前言
錨點目錄定位功能在長頁面和文檔類網(wǎng)站中非常常見,它可以讓用戶快速定位到頁面中的某個章節(jié)
- 如何在React中實現(xiàn)錨點定位和平滑滾動
- 目錄自動高亮的實現(xiàn)思路
- 處理頂部導航遮擋錨點的解決方案
- 服務端渲染下的實現(xiàn)方案
- 性能優(yōu)化策略
實現(xiàn)基本錨點定位
首先,我們需要實現(xiàn)頁面內基本的錨點定位功能。對于錨點定位來說,主要涉及這兩個部分:
- 設置錨點,為頁面中的某個組件添加id屬性
- 點擊鏈接,跳轉到指定錨點處

例如:
// 錨點組件
function AnchorComponent() {
return <h2 id="anchor">This is anchor</h2>
}
// 鏈接組件
function LinkComponent() {
return (
<a href="#anchor" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Jump to Anchor</a>
)
}當我們點擊Jump to Anchor這個鏈接時,頁面會平滑滾動到AnchorComponent所在的位置。
使用useScrollIntoView自定義hook
React中實現(xiàn)錨點定位,最簡單的方式就是使用useScrollIntoView這個自定義hook。
import { useScrollIntoView } from 'react-use';
function App() {
const anchorRef = useRef();
const scrollToAnchor = () => {
useScrollIntoView(anchorRef);
}
return (
<>
<a href="#anchor" rel="external nofollow" rel="external nofollow" rel="external nofollow" onClick={scrollToAnchor}>
Jump to Anchor
</a>
<h2 id="anchor" ref={anchorRef}>This is anchor</h2>
</>
)
}useScrollIntoView接受一個ref對象,當調用這個hook函數(shù)時,會自動滾動頁面,使得ref對象在可視區(qū)域內。
原生scrollIntoView方法
useScrollIntoView內部其實就是使用了原生的scrollIntoView方法,所以我們也可以直接調用:
function App() {
const anchorRef = useRef();
const scrollToAnchor = () => {
anchorRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
};
return (
<>
<a href="#anchor" rel="external nofollow" rel="external nofollow" rel="external nofollow" onClick={scrollToAnchor}>Jump to Anchor</a>
<h2 id="anchor" ref={anchorRef}>This is anchor</h2>
</>
)
}scrollIntoView可以讓元素的父容器自動滾動,將這個元素滾動到可見區(qū)域。behavior:'smooth'可以啟用平滑滾動效果。
錨點定位和目錄聯(lián)動
很多時候,我們會在頁面中實現(xiàn)一個目錄導航,可以快速定位到各個章節(jié)。此時就需要實現(xiàn)錨點定位和目錄的聯(lián)動效果:
- 點擊目錄時,自動滾動到對應的章節(jié)
- 滾動頁面時,自動高亮正在瀏覽的章節(jié)
目錄導航組件
目錄導航本身是一個靜態(tài)組件,我們通過props傳入章節(jié)數(shù)據(jù):
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屬性建立章節(jié)內容和目錄鏈接之間的關聯(lián)。
處理點擊事件
當點擊目錄鏈接時,需要滾動到對應的章節(jié)位置:
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獲取到元素,并滾動到可視區(qū)域,實現(xiàn)平滑跳轉。
自動高亮
實現(xiàn)自動高亮也很簡單,通過監(jiān)聽滾動事件,計算章節(jié)元素的偏移量,判斷哪個章節(jié)在可視區(qū)域內,并更新active狀態(tài):
function App() {
const [activeChapter, setActiveChapter] = useState();
useEffect(() => {
const handleScroll = () => {
chapters.forEach(chapter => {
const element = document.getElementById(chapter.id);
// 獲取元素在可視區(qū)域中的位置
const rect = element.getBoundingClientRect();
// 判斷是否在可視區(qū)域內
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可以得到元素相對于視窗的位置信息,根據(jù)位置判斷是否在可見區(qū)域內,如果是就更新activeChapter狀態(tài),從而觸發(fā)目錄的高亮效果。
問題解析
遮擋問題
有時錨點會被固定的Header遮擋,此時滾動會定位到元素上方,用戶看不到錨點對應的內容。
常見的解決方案是:
- 設置錨點元素margin-top
#anchor {
margin-top: 80px; /* header高度 */
}直接設置一個和Header高度相同的margin,來防止遮擋。
- 在滾動方法中加入offset
// scroll offset
const scrollOffset = -80;
chapterEl.scrollIntoView({
offsetTop: scrollOffset
})給scrollIntoView傳入一個頂部偏移量,這樣也可以跳過Header的遮擋。
響應式問題
在響應式場景下,目錄的遮擋問題會更復雜。我們需要區(qū)分不同斷點下,計算匹配的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
})
}
//...
}根據(jù)不同斷點,動態(tài)計算滾動偏移量,這樣可以適配所有情況。
性能優(yōu)化
使用節(jié)流
滾動事件會高頻觸發(fā),直接在滾動回調中計算章節(jié)位置會造成性能問題。
我們可以使用Lodash的throttle函數(shù)進行節(jié)流:
import throttle from 'lodash.throttle';
const handleScroll = throttle(() => {
// 計算章節(jié)位置
}, 100);這樣可以限制滾動事件最多每100ms觸發(fā)一次。
IntersectionObserver
使用IntersectionObserver提供的異步回調,只在章節(jié)進入或者離開可視區(qū)域時才執(zhí)行位置計算:
import { useRef, useEffect } from 'react';
function App() {
const chaptersRef = useRef({});
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
// 章節(jié)進入或者離開可視區(qū)域時更新
}
);
chapters.forEach(chapter => {
observer.observe(
document.getElementById(chapter.id)
);
})
}, []);
} 這種懶加載式的方式可以大幅減少無效的位置計算。
SSR支持
在Next.js等SSR場景下,客戶端腳本會延后加載,頁面初次渲染時目錄聯(lián)動會失效。
getInitialProps注水
可以在getInitialProps中提前計算目錄數(shù)據(jù),注入到頁面中:
Home.getInitialProps = async () => {
const chapters = await fetchChapters();
const mappedChapters = chapters.map(chapter => {
return {
...chapter,
highlighted: isChapterHighlighted(chapter)
}
});
return {
chapters: mappedChapters
};
};hydrate處理
客戶端腳本加載后,需要調用ReactDOM.hydrate而不是render方法,進行數(shù)據(jù)的補充填充,避免目錄狀態(tài)丟失。
import { useEffect } from 'react';
function App({ chapters }) {
useEffect(() => {
ReactDOM.hydrate(
<App chapters={chapters} />,
document.getElementById('root')
);
}, []);
}服務端渲染的實現(xiàn)方案

在使用了服務端渲染(SSR)的框架如Next.js等情況下,實現(xiàn)錨點定位和目錄聯(lián)動也會有一些不同。
主要區(qū)別在于:
- 服務端和客戶端環(huán)境不統(tǒng)一
- 腳本加載時間差
這會導致一些狀態(tài)錯位的問題。
問題復現(xiàn)
假設我們有下面的目錄和內容結構:
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環(huán)境下,點擊鏈接和滾動都可以正常工作。
但是在Next.js的SSR環(huán)境下就會有問題:
點擊目錄鏈接時,頁面不會滾動。
這是因為在服務端,我們無法獲取組件的ref,所以錨點元素不存在,自然無法定位。
滾動頁面時,目錄高亮也失效。
服務端渲染的靜態(tài)HTML中,并沒有綁定滾動事件,所以無法自動高亮。
預取數(shù)據(jù)
首先,我們需要解決點擊目錄鏈接的問題。
既然服務端無法獲取組件ref,那就需要在客戶端去獲取元素位置。
這里有兩個方法:
- 組件掛載后主動緩存元素位置
// Chapter組件
useEffect(() => {
// 緩存位置數(shù)據(jù)
cacheElementPosition(chapter.id, ref.current);
}, []);
// Utils
const elementPositions = {};
function cacheElementPosition(id, element) {
const rect = element.getBoundingClientRect();
elementPositions[id] = {
left: rect.left,
top: rect.top,
}
}- 點擊時實時獲取元素位置
// handle link click
const scrollToChapter = (chapterId) => {
const element = document.getElementById(chapterId);
const rect = element.getBoundingClientRect();
window.scrollTo({
top: rect.top,
behavior: 'smooth'
})
}無論哪種方法,都需要在組件掛載后獲取元素的位置信息。
這樣我們就可以在點擊目錄鏈接時,正確滾動到對應的章節(jié)位置了。
數(shù)據(jù)注水
但是點擊目錄只解決了一半問題,滾動高亮還需要解決。
這里就需要用到數(shù)據(jù)注水的技術。
簡單來說就是:
- 在服務端渲染時,讀取路由參數(shù),提前計算高亮狀態(tài)
- 將高亮數(shù)據(jù)注入到響應中
- 客戶端拿到注水的數(shù)據(jù)后渲染,不會出現(xiàn)高亮錯位
實現(xiàn)步驟:
1.服務端獲取參數(shù)和數(shù)據(jù)
// 在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>
))}
</以上就是在React項目中實現(xiàn)一個簡單的錨點目錄定位的詳細內容,更多關于React實現(xiàn)錨點目錄定位的資料請關注腳本之家其它相關文章!
相關文章
React Native使用百度Echarts顯示圖表的示例代碼
本篇文章主要介紹了React Native使用百度Echarts顯示圖表的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11

