Scrollspy 滾動監控,讓網頁根據捲軸位置自動更新選單
網頁開發這次網站改版,我想在文章右邊加入目錄,並且讓目錄可以根據卷軸位置自動更新顯示,這種功能稱為滾動監聽 (Scrollspy)。 Scrollspy 技術很早以前就有了,算不上什麼新技術,所以我本來打算直接使用 Bootstrap 內建的 Scrollspy 製作。 在製作過程中,我一直遇到觸發更新事件的捲軸位置不正確的問題。似乎 HTML 結構一定要符合 Bootstrap 要求的結構才能正常運作,但網站上已經有很多文章,每篇都進行調整的工作量太大了。
因此,我花時間研究了一下 Scrollspy 原理,並決定自己開發此功能,本文將分享我的製作思路,提供給大家參考。 目前文章右邊的目錄就是使用本文分享的方式製作,但您也能點擊下方連結觀看成果:
思路一:使用 onscroll 事件
第一時間想到最簡單的方式就是使用 onscroll 事件,在事件中判斷卷軸到達哪些區塊,並更新相對應的目錄。 這種做法只要判斷寫得好,可以很精確的控制更新選單的時機。 缺點則是比較耗效能,因為 onscroll 是連續觸發的事件,每次捲動卷軸就會一直連續觸發事件。 因此,我並沒有使用這個方法方式。
思路二:使用 Intersection Observer API
Intersection Observer 採用非同步執行,不會隨著卷軸捲動一直觸發事件。 雖然官方文件說 Intersection Observer 沒辦法精確到像素,只能偵測大概範圍,但拿來做 Scrollspy 已經十分夠用了。
此外,我看很多文章都說 Intersection Observer 底層使用 window.requestIdleCallback() 來觸發 callback(),這表示只有當瀏覽器空閒下來才會執行 callback() 中的程式碼,優先層級較低,較不吃效能。 不過我並沒有找到這段話的相關證據,查看 IntersectionObserver polyfill 好像也沒有相關的程式碼,如果有人知道這段話證據在哪裡的話,歡迎留言告訴我。
基本用法
let options = {
root: document.querySelector('#scroll'),
rootMargin: '0px 0px 0px 0px',
threshold: 1.0,
};
let callback = (entries, observer) => {
for (const entry of entries) {
if (entry.isIntersecting) {
}
}
};
let observer = new IntersectionObserver(callback, options);
observer.observe(section);
-
行 1 ~ 13:
建立 IntersectionObserver 物件時,需要兩個參數 options 和 callback。 options 包含三個參數:root、rootMargin 和 threshold,這些在下一個小節會進一步詳細說明。 callback 是當觀察區塊進入或離開目標範圍時要執行的程式碼,您可以使用 entry.isIntersecting 來判斷被觀察區塊是進入目標還是離開目標。 -
行 14:
當 IntersectionObserver 建立後,將 section 加入觀察對象,也就是說當 section 進入或離開目標範圍時,就會觸發 callback()。
重要參數
- root
- 指定 viewport,也就是當觀察區塊進入該區域觸發 callback()。預設值為 null,表示 viewport 為最外層的瀏覽器。
- rootMargin
- viewport 的偵測範圍預設與 viewport 的長寬一致,當 rootMargin 數值為正數的話,表示擴大偵測範圍,如果為負數的話,則是縮小偵測範圍。 此外,rootMargin 與 CSS 的 margin 類似,有四個數字表示上右下左的 margin 數值,也能使用百分比。
- threshold
- 參數值為 0 ~ 1,表示被觀察區塊有多少百分比進入偵測範圍內才觸發 callback(),預設值為 0,表示只要被觀察區塊有 1 個像素進入偵測區域就觸發。 這個參數也能使用陣列,例如 [0.25, 0.5, 0.75, 1] 表示當被觀察區塊 25%、50%、75%、100% 進入都觸發 callback()。
實作
了解原理之後,現在可以開始動手實作了。 首先,我們需要先製作一個導覽列及內容區塊。 在下面的範例中,我們假設有四個內容區塊,所以導覽列中也要有四個連結,HTML 大致如下。
<ul class="nav nav-pills nav-fill">
<li class="nav-item"><a href="#section1" class="nav-link">區塊一</a></li>
<li class="nav-item"><a href="#section2" class="nav-link">區塊二</a></li>
<li class="nav-item"><a href="#section3" class="nav-link">區塊三</a></li>
<li class="nav-item"><a href="#section4" class="nav-link">區塊四</a></li>
</ul>
<div class="scrollspy">
<div id="section1" class="section">區塊一</div>
<div id="section2" class="section">區塊二</div>
<div id="section3" class="section">區塊三</div>
<div id="section4" class="section">區塊四</div>
</div>
接著,使用 CSS 稍微美化一下,這邊只是稍微美化一下而已,並且將每個區塊高度設定為 100%,以方便展示。 當然,您可以根據自己的設計需求進行更進一步的調整。
.nav {
background: #CCCCCC;
}
.nav .nav-item .nav-link {
border-radius: 0px;
}
.nav .nav-item .nav-link.active {
background: #0D6EFD;
}
.scrollspy {
height: calc(100vh - 520px);
border: 1px solid #CCCCCC;
overflow: auto;
scroll-behavior: smooth;
}
.scrollspy .section {
padding: 15px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 2rem;
}
.scrollspy .section:nth-child(odd) {
background-color: pink;
}
.scrollspy .section:nth-child(even) {
background-color: yellow;
}
最後是 JavaScript 的部分。
window.addEventListener('load', (event) => {
const content = document.querySelector('.scrollspy');
const directory = document.querySelectorAll('.nav li a');
const navLinks = [];
const options = {
root: content,
rootMargin: '-5% 0px -95% 0px',
threshold: 0
};
const callback = (entries) => {
for (const entry of entries) {
const id = entry.target.id
if (entry.isIntersecting) {
navLinks[id].classList.add('active');
} else {
navLinks[id].classList.remove('active');
}
}
};
const observer = new IntersectionObserver(callback, options);
directory.forEach((elem) => {
const id = elem.getAttribute('href').substr(1);
const section = content.querySelector('#' + id);
observer.observe(section);
navLinks[id] = elem;
})
})
-
行 2:
取得 scrollspy 區塊,這個區塊是所有被觀察區塊的外層,具有捲軸,會在行 6 的 root 參數設定為 viewport。 -
行 3:
取得導覽列中的連結,後續會根據連結的 href 值取得相對應的被觀察者。 -
行 5 ~ 9:
建立 options,將 content 設定為 viewport,rootMargin 設定為 -5% 0px -95% 0px,讓每個區塊到達上方約 5% 的位置才觸發 callback()。 -
行 10 ~ 19:
建立 callback,當觀察區塊進入 viewport 時,將相對應的導覽列連結加入 active Class。反之,則移除 active Class。 -
行 20:
建立 IntersectionObserver 物件。 -
行 20 ~ 26:
根據 directory 中連結的 href 值取得相對應的 section 元素,並將 section 加入觀察對象。同時將連結物件,使用 id 作為索引值加入 navLinks 中,這是為了方便在 callback() 操作。
結語
到這邊就全部完成了,相信應該不會太難。 我覺得最麻煩的是當版面很複雜的時候,要調整 rootMargin 觸發 callback() 時,需要花點時間嘗試不同的參數,才能有最好的效果。 如果實作上有遇到什麼困難,或者您覺得有什麼更好的做法,都歡迎留言討論喔。
0 則留言