在React項目中實現(xiàn)一個簡單的錨點目錄定位
前言
錨點目錄定位功能在長頁面和文檔類網(wǎng)站中非常常見,它可以讓用戶快速定位到頁面中的某個章節(jié)
- 如何在React中實現(xiàn)錨點定位和平滑滾動
- 目錄自動高亮的實現(xiàn)思路
- 處理頂部導(dǎo)航遮擋錨點的解決方案
- 服務(wù)端渲染下的實現(xiàn)方案
- 性能優(yōu)化策略
實現(xiàn)基本錨點定位
首先,我們需要實現(xiàn)頁面內(nèi)基本的錨點定位功能。對于錨點定位來說,主要涉及這兩個部分:
- 設(shè)置錨點,為頁面中的某個組件添加id屬性
- 點擊鏈接,跳轉(zhuǎn)到指定錨點處
例如:
// 錨點組件 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> ) }
當(dāng)我們點擊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對象,當(dāng)調(diào)用這個hook函數(shù)時,會自動滾動頁面,使得ref對象在可視區(qū)域內(nèi)。
原生scrollIntoView方法
useScrollIntoView內(nèi)部其實就是使用了原生的scrollIntoView方法,所以我們也可以直接調(diào)用:
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)一個目錄導(dǎo)航,可以快速定位到各個章節(jié)。此時就需要實現(xiàn)錨點定位和目錄的聯(lián)動效果:
- 點擊目錄時,自動滾動到對應(yīng)的章節(jié)
- 滾動頁面時,自動高亮正在瀏覽的章節(jié)
目錄導(dǎo)航組件
目錄導(dǎo)航本身是一個靜態(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é)內(nèi)容和目錄鏈接之間的關(guān)聯(lián)。
處理點擊事件
當(dāng)點擊目錄鏈接時,需要滾動到對應(yīng)的章節(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回調(diào),當(dāng)點擊鏈接時,通過chapterId獲取到元素,并滾動到可視區(qū)域,實現(xiàn)平滑跳轉(zhuǎn)。
自動高亮
實現(xiàn)自動高亮也很簡單,通過監(jiān)聽滾動事件,計算章節(jié)元素的偏移量,判斷哪個章節(jié)在可視區(qū)域內(nèi),并更新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ū)域內(nèi) 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ū)域內(nèi),如果是就更新activeChapter狀態(tài),從而觸發(fā)目錄的高亮效果。
問題解析
遮擋問題
有時錨點會被固定的Header遮擋,此時滾動會定位到元素上方,用戶看不到錨點對應(yīng)的內(nèi)容。
常見的解決方案是:
- 設(shè)置錨點元素margin-top
#anchor { margin-top: 80px; /* header高度 */ }
直接設(shè)置一個和Header高度相同的margin,來防止遮擋。
- 在滾動方法中加入offset
// scroll offset const scrollOffset = -80; chapterEl.scrollIntoView({ offsetTop: scrollOffset })
給scrollIntoView傳入一個頂部偏移量,這樣也可以跳過Header的遮擋。
響應(yīng)式問題
在響應(yīng)式場景下,目錄的遮擋問題會更復(fù)雜。我們需要區(qū)分不同斷點下,計算匹配的offset。
可以通過MatchMedia Hook獲取當(dāng)前的斷點:
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ā),直接在滾動回調(diào)中計算章節(jié)位置會造成性能問題。
我們可以使用Lodash的throttle函數(shù)進(jìn)行節(jié)流:
import throttle from 'lodash.throttle'; const handleScroll = throttle(() => { // 計算章節(jié)位置 }, 100);
這樣可以限制滾動事件最多每100ms觸發(fā)一次。
IntersectionObserver
使用IntersectionObserver提供的異步回調(diào),只在章節(jié)進(jìn)入或者離開可視區(qū)域時才執(zhí)行位置計算:
import { useRef, useEffect } from 'react'; function App() { const chaptersRef = useRef({}); useEffect(() => { const observer = new IntersectionObserver( (entries) => { // 章節(jié)進(jìn)入或者離開可視區(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處理
客戶端腳本加載后,需要調(diào)用ReactDOM.hydrate而不是render方法,進(jìn)行數(shù)據(jù)的補充填充,避免目錄狀態(tài)丟失。
import { useEffect } from 'react'; function App({ chapters }) { useEffect(() => { ReactDOM.hydrate( <App chapters={chapters} />, document.getElementById('root') ); }, []); }
服務(wù)端渲染的實現(xiàn)方案
在使用了服務(wù)端渲染(SSR)的框架如Next.js等情況下,實現(xiàn)錨點定位和目錄聯(lián)動也會有一些不同。
主要區(qū)別在于:
- 服務(wù)端和客戶端環(huán)境不統(tǒng)一
- 腳本加載時間差
這會導(dǎo)致一些狀態(tài)錯位的問題。
問題復(fù)現(xiàn)
假設(shè)我們有下面的目錄和內(nèi)容結(jié)構(gòu):
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)境下就會有問題:
點擊目錄鏈接時,頁面不會滾動。
這是因為在服務(wù)端,我們無法獲取組件的ref,所以錨點元素不存在,自然無法定位。
滾動頁面時,目錄高亮也失效。
服務(wù)端渲染的靜態(tài)HTML中,并沒有綁定滾動事件,所以無法自動高亮。
預(yù)取數(shù)據(jù)
首先,我們需要解決點擊目錄鏈接的問題。
既然服務(wù)端無法獲取組件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' }) }
無論哪種方法,都需要在組件掛載后獲取元素的位置信息。
這樣我們就可以在點擊目錄鏈接時,正確滾動到對應(yīng)的章節(jié)位置了。
數(shù)據(jù)注水
但是點擊目錄只解決了一半問題,滾動高亮還需要解決。
這里就需要用到數(shù)據(jù)注水的技術(shù)。
簡單來說就是:
- 在服務(wù)端渲染時,讀取路由參數(shù),提前計算高亮狀態(tài)
- 將高亮數(shù)據(jù)注入到響應(yīng)中
- 客戶端拿到注水的數(shù)據(jù)后渲染,不會出現(xiàn)高亮錯位
實現(xiàn)步驟:
1.服務(wù)端獲取參數(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)一個簡單的錨點目錄定位的詳細(xì)內(nèi)容,更多關(guān)于React實現(xiàn)錨點目錄定位的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React Native使用百度Echarts顯示圖表的示例代碼
本篇文章主要介紹了React Native使用百度Echarts顯示圖表的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11詳解React+Koa實現(xiàn)服務(wù)端渲染(SSR)
這篇文章主要介紹了詳解React+Koa實現(xiàn)服務(wù)端渲染(SSR),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05