一些(網站)滾動的效果是如此令人著迷但你卻不知該如何實現,本文將為你揭開它們的神祕面紗。我們將基於最新的技術與規範為你介紹最新的 JavaScript 與 CSS 特性,(當你付諸實踐時,)將使你的頁面滾動更平滑、美觀且效能更好。
大多數的網頁的內容都無法在一屏內全部展現,因而(頁面)滾動對於使用者而言是必不可少的。對於前端工程師與 UX 設計師而言,跨瀏覽器提供良好的滾動體驗,同時符合設計(要求),無疑是一個挑戰。儘管 web 標準的發展速度遠超從前,但程式碼的實現往往是落後的。下文將為你介紹一些常見的關於滾動的案例,檢查一下你所用的解決方案是否被更優雅的方案所代替。
消逝的滾動條
在過去的三十年裡,滾動條的外觀不斷改變以符合設計的趨勢,設計師們為(滾動條的)顏色、陰影、上下箭頭的形狀與邊框的圓角實驗了多種風格。以下是 Windows 上的變化歷程:
(Windows 上的滾動條)
在2011年,蘋果設計師從 ios 上獲得靈感,為如何定義“美觀的”滾動條確定了方向。所有滾動條均從 Mac 電腦中消失,不再佔據任何頁面空間,只有在使用者觸發滾動時(滾動條)才會重新出現(有些使用者會設定不隱藏滾動條)。
(Mac 上的滾動條)
滾動條安靜地消逝並未引起蘋果粉絲的不滿,已經習慣了 iPhone 與 iPad 上滾動方式的使用者很快地習慣了這一設計。大多數開發人員與設計師都認為這是一個“好訊息”,因為計算滾動條的寬度可真是件苦差事。
然而,我們生活在一個擁有眾多作業系統與瀏覽器的世界中,它們(對於滾動)的實現各不相同。如果你和我們一樣是一名 Web 開發者,你可不能把“滾動條問題”置之不理。
以下將為你介紹一些小技巧,使你的使用者在滾動時有更好的體驗。
隱藏但可滾動
先來看看一個關於模態框的經典例子。當它被開啟的時候,主頁面應該停止滾動。在 CSS 中有如下的快捷實現方式:
1 2 3 |
body { overflow: hidden; } |
但上述程式碼會帶來一點不良的副作用:
(注意紅色剪頭)
在這個示例中,為了演示目的,我們在 Mac 系統中設定了強制顯示滾動條,因而使用者體驗與 Windows 使用者相似。
我們該如何解決這個問題呢?如果我們知道滾動條的寬度,每次當模態框出現時,可在主頁面的右邊設定一點邊距。
由於不同的作業系統與瀏覽器對滾動條的寬度不一,因而獲取它的寬度並不容易。在Mac 系統中,無論任何瀏覽器(滾動條)都是統一15px,然而 Windows 系統可會令開發者發狂:
(“百花齊放”的寬度)
注意,以上僅是 Windows 系統下基於當前最新版瀏覽器(測試所得)的結果。以前的(瀏覽器)版本(寬度)可能有所不同,也沒人知道未來(滾動條的寬度)會如何變化。
不同於猜測(滾動條的寬度),你可以通過 JavaScript 計算它的寬度(譯者注:實測以下程式碼僅能測出原始的寬度,通過 CSS 改變了滾動條寬度後,以下程式碼也無法測出實際寬度):
1 2 3 4 5 6 7 |
const outer = document.createElement('div'); const inner = document.createElement('div'); outer.style.overflow = 'scroll'; document.body.appendChild(outer); outer.appendChild(inner); const scrollbarWidth = outer.offsetWidth - inner.offsetWidth; document.body.removeChild(outer); |
儘管僅僅七行程式碼(就能測出滾動條的寬度),但有數行程式碼是操作 DOM 的。(為效能起見,)如非必要,儘量避免進行 DOM 操作。
解決這個問題的另一個方法是在模態框出現時仍保留滾動條,以下是基於這思路的純 CSS 實現:
1 2 3 |
html { overflow-y: scroll; } |
儘管“模態框抖動”問題解決了,但整體的外觀卻被一個無法使用的滾動條影響了,這無疑是設計中的硬傷。
在我們看來,更好的解決方案是完全地隱藏滾動條。純粹用 CSS 也是可以實現的。該方法(達到的效果)和 macOS 的表現並不是完全一致,(當使用者)滾動時滾動條仍然是不可見的。滾動條總是處於不可見狀態,然而頁面是可被滾動的。對於Chrome,Safari 和 Opera 而言,可以使用以下的 CSS:
1 2 3 |
.container::-webkit-scrollbar { display: none; } |
IE 或 Edge 可用以下程式碼:
1 2 3 |
.container { -ms-overflow-style: none; } |
至於 Firefox,很不幸,沒有任何辦法隱藏滾動條。
正如你所見,並沒有任何銀彈。任何解決方案都有它的優點與缺點,應根據你專案的需要選擇最合適的。
外觀爭議
需要承認的是,滾動條的樣子在部分作業系統上並不好看。一些設計師喜歡完全掌控他們(所設計)應用的樣式,任何一絲細節也不放過。在 GitHub 上有上百個庫藉助 JavaScript 取代系統滾動條的預設實現,以達到自定義的效果。
但如果你想根據現有的瀏覽器定製一個滾動條呢?(很遺憾,)並沒有通用的 API,每個瀏覽器都有其獨特的程式碼實現。
儘管5.5版本以後的 IE 瀏覽器允許你修改滾動條的樣式,但它只允許你修改滾動條的顏色。以下是如何重新繪製(滾動條)拖動部分與箭頭的程式碼:
1 2 3 |
body { scrollbar-face-color: blue; } |
但只改變顏色對提高使用者體驗而言幫助不大。據此,WebKit 的開發者在2009年提出了(修改滾動條)樣式的方案。以下是使用 -webkit
字首在支援相關樣式的瀏覽器中模擬 macOS 滾動條樣式的程式碼:
1 2 3 4 5 6 7 |
::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-thumb { background-color: #c1c1c1; border-radius: 4px; } |
Chrome、Safari、Opera 甚至於 UC 瀏覽器或者三星自帶的桌面瀏覽器都支援(上述 CSS)。Edge 也有計劃實現它們。但三年過去了,該計劃仍在中等優先順序中(而尚未被實現)。
當我們討論滾動條的定製時,Mozilla 基金會基本上是無視了設計師的需求。(有開發者在)17年前就已經提出了一個希望修改滾動條樣式的請求。而就在幾個月前,Jeff Griffiths(Firefox 瀏覽器總監)終於為這個問題作出了回答:
“除非團隊中有人對此有興趣,否則我對此毫不關心。”
公平地說,從 W3C 的角度看來,儘管 WebKit 的實現得到廣泛的支援,但它仍然不是標準。現有的為滾動條修改樣式的草案,是基於 IE 的:僅能修改它的顏色。
伴隨著請求如同 WebKit 一樣支援滾動條樣式修改 issue 的提交,爭議仍在繼續。如果你想影響 CSS 工作小組,是時候參與討論了。也許這不是優先順序最高的問題,但(如同 WebKit 一樣修改滾動條樣式)得到標準化後,能使很多前端工程師與設計師輕鬆很多。
流暢的操作體驗
對於滾動而言,最常見的任務是登入頁的導航(跳轉)。通常,它是通過錨點連結來完成的。只需要知道元素的 id
即可:
1 |
<a href="#section">Section</a> |
點選該連結會 跳 到(該錨點對應的)區塊上,(然而) UX 設計師一般會堅持認為該過程應是平滑地運動的。GitHub 上有大量造好的輪子(幫你解決這個問題),然而它們或多或少都用到 JavaScript。(其實)只用一行程式碼也能實現同樣的效果,最近DOM API 中的 Element.scrollIntoView()
可以通過傳入配置物件來實現平滑滾動:
1 2 3 |
elem.scrollIntoView({ behavior: 'smooth' }); |
然而該屬性相容性較差且仍是通過指令碼(來控制樣式)。如有可能,應儘量少用額外的指令碼。
幸運的是,有一個全新的 CSS 屬性(仍在工作草案中),可以用簡單的一行程式碼改變整個頁面滾動的行為。
1 2 3 |
html { scroll-behavior: smooth; } |
結果如下:
(從一個區塊跳到另一個)
(平滑地滾動)
你可以在 codepen 上試驗這個屬性。在撰寫本文時,scroll-behavior
僅在 Chrome、 Firefox 與 Opera 上被支援,但我們希望它能被廣泛支援,因為使用 CSS (比使用 JavaScript)在解決頁面滾動問題時優雅得多,並更符合“漸進增強”的模式。
粘性 CSS
另一個常見的需求是根據滾動方向動態地定住元素,即有名的“粘性(即 CSS 中的position: sticky
)”效應。
(一個粘性元素)
在以前的日子裡,要實現一個“粘性”元素需要編寫複雜的滾動處理函式去計算元素的大小。(然而)該函式較難處理元素在“黏住”與“不黏住”之間微小的延遲,(通常會)導致(元素)抖動的出現。通過 JavaScript 來實行(“粘性”元素)也有效能上的問題,特別是在(需要)呼叫 [Element.getBoundingClientRect()
]時(https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)。
不久之前,CSS 實現了 position: sticky
屬性。只需通過指定(某方向上的)偏移量即可實現我們想要的效果。
1 2 3 4 |
.element { position: sticky; top: 50px; } |
(編寫上述程式碼後,)剩下的就交由瀏覽器實現即可。你可以在 codepen 上試驗一下。撰寫本文之時,position: sticky
在各式瀏覽器(包括移動端瀏覽器)上支援良好,所以如果你還在使用 JavaScript 去解決這個問題的話,是時候換成純 CSS 的實現了。
全面使用函式節流
從瀏覽器的角度看來,滾動是一個事件,因此在 JavaScript 中是使用一個標準化的事件監聽器 addEventListener
去處理它: ,
1 2 3 4 |
window.addEventListener('scroll', () => { const scrollTop = window.scrollY; /* doSomething with scrollTop */ }); |
使用者往往高頻率地滾動(頁面),但如果滾動事件觸發太頻繁的話,會導致效能上的問題,可以通過使用函式節流這一技巧去優化它。
1 2 3 4 |
window.addEventListener('scroll', throttle(() => { const scrollTop = window.scrollY; /* doSomething with scrollTop */ })); |
你需要定義一個節流函式包裝原來的事件監聽函式,(節流函式是)減少被包裝函式的執行次數,只允許它在固定的時間間隔之內執行一次:
1 2 3 4 5 6 7 8 9 |
function throttle(action, wait = 1000) { let time = Date.now(); return function() { if ((time + wait - Date.now()) < 0) { action(); time = Date.now(); } } } |
為了使(節流後的)滾動更平滑,你可以通過使用 window.requestAnimationFrame()
來實現函式節流:
1 2 3 4 5 6 7 8 9 10 11 |
function throttle(action) { let isRunning = false; return function() { if (isRunning) return; isRunning = true; window.requestAnimationFrame(() => { action(); isRunning = false; }); } } |
當然,你可以通過現有的開源輪子來實現,就像 Lodash 一樣。你可以訪問 codepen 來看看上述解決方案與 Lodash 中的 _.throttle
之間的區別。
使用哪個(開源庫)並不重要,重要的是在需要的時候,記得優化你(頁面中的)滾動處理函式。
在視窗中顯示
當你需要實現圖片懶載入或者無限滾動時,需要確定元素是否出現在視窗中。這可以在事件監聽器中處理,最常見的解決方案是使用 lement.getBoundingClientRect()
:
1 2 3 4 5 6 |
window.addEventListener('scroll', () => { const rect = elem.getBoundingClientRect(); const inViewport = rect.bottom > 0 && rect.right > 0 && rect.left < window.innerWidth && rect.top < window.innerHeight; }); |
上述程式碼的問題在於每次呼叫 getBoundingClientRect
時都會觸發迴流,嚴重地影響了效能。在事件處理函式中呼叫( getBoundingClientRect
)尤為糟糕,就算使用了函式節流(的技巧)也可能對效能沒多大幫助。 (迴流是指瀏覽器為區域性或整體地重繪某個元素,需要重新計算該元素在文件中的位置與形狀。)
在2016年後,可以通過使用 Intersection Observer 這一 API 來解決問題。它允許你追蹤目標元素與其祖先元素或視窗的交叉狀態。此外,儘管只有一部分元素出現在視窗中,哪怕只有一畫素,也可以選擇觸發回撥函式:
1 2 3 |
const observer = new IntersectionObserver(callback, options); observer.observe(element); |
(點選這裡,檢視觸發迴流的 DOM 屬性和方法。)
此 API 被廣泛地支援,但仍有一些瀏覽器需要 polyfill。儘管如此,它仍是目前最好的解決方案。
滾動邊界問題
如果你的彈框或下拉選單是可滾動的,那你務必要了解連鎖滾動相關的問題:當使用者滾動到(彈框或下拉選單)末尾(後再繼續滾動時),整個頁面都會開始滾動。
(連鎖滾動的表現)
當滾動元素到達底部時,你可以通過(改變)頁面的 overflow
屬性或在滾動元素的滾動事件處理函式中取消預設行為來解決這問題。
如果你選擇使用 JavaScript (來處理),請記住要處理的不是“scroll(事件)”,而是每當使用者使用滑鼠滾輪或觸控板時觸發的“wheel(事件)”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function handleOverscroll(event) { const delta = -event.deltaY; if (delta< 0 && elem.scrollHeight - elem.scrollTop) { elem.scrollTop = elem.scrollHeight; event.preventDefault(); return false; } if (delta > elem.scrollTop) { elem.scrollTop = 0; event.preventDefault(); return false; } return true; } |
不幸的是,這個解決方案不太可靠。同時可能對(頁面)效能產生負面影響。
過度滾動對移動端的影響尤為嚴重。Loren Brichter 在 iOS 的 Tweetie 應用上創造了一個“下拉重新整理”的新手勢,這在 UX 社群中引起了轟動:包括 Twitter 與 Facebook 在內的各大應用紛紛採用了(相同的手勢)。
當這個特性出現在安卓端的 Chrome 瀏覽器中時,問題出現了:它會重新整理整個頁面而不是載入更多的內容,成為開發者在他們的應用中實現“下拉重新整理”時的麻煩。
CSS 通過 overscroll-behavior
這個新屬性解決問題。它通過控制元素滾動到盡頭時的行為來解決下拉重新整理與連鎖滾動所帶來的問題,(它的屬性值中)也包含針對不同平臺特殊值:安卓的 glow
與 蘋果系統中的 rubber band
。
現在,上面 GIF 中的問題,在 Chrome、Opera 或 Firefox 中可以通過以下一行程式碼來解決:
1 2 3 |
.element { overscroll-behavior: contain; } |
公平地說,IE 與 Edge 實現了(它獨有的) -ms-scroll-chaining
屬性來控制連鎖滾動,但它並不能處理所有的情況。幸運的是,根據這訊息,微軟的瀏覽器已經準備實現 overscroll-behavior
這一屬性了。
觸屏之後
觸屏裝置上的滾動(體驗)是一個很大的話題,深入討論需要另開一篇文章。然而,由於很多開發者忽略了這方面的內容,這裡需要提及一下。
(滾動手勢無處不在,令人沉迷,以至於想出了如此瘋狂的主意去解決“滾動上癮”的問題。)
周圍的人在智慧手機螢幕上上下移動他們的手指的頻率是多少呢?經常這樣對吧,當你閱讀本文時,你很可能就在這麼做。
當你的手指在螢幕上移動時,你期待的是:頁面內容平滑且流暢地移動。
蘋果公司開創了“慣性”滾動並擁有它的專利 。它訊速地成為了使用者互動的標準並且我們對此已習以為常。
但你也許已經注意到了,儘管移動端系統會為你實現頁面上的慣性滾動,但當頁面內某個元素發生滾動時,即使使用者同樣期待慣性滾動,但它並不會出現,這令人沮喪。
這裡有一個 CSS 的解決方案,但看起來更像是個 hack:
1 2 3 |
.element { -webkit-overflow-scrolling: touch; } |
為什麼這是個 hack 呢?首先,它只能在支援(webkit)字首的瀏覽器上才能工作。其次,它只適用於觸屏裝置。最後,如果瀏覽器不支援的話,你就這樣置之不理嗎?但無論如何,這總歸是一個解決方案,你可以試著使用它。
在觸屏裝置上,另一個需要考慮的問題是開發者如何處理 touchstart
與 touchmove
事件觸發時可能存在的效能問題,它對使用者滾動體驗的影響非常大。這裡詳細描述了整個問題。簡單來說,現代的瀏覽器雖然知道如何使得滾動變得平滑,但為確認(滾動)事件處理函式中是否執行了 Event.preventDefault()
以取消預設行為,有時仍可能需要花費500毫秒來等待事件處理函式執行完畢。
即使是一個空的事件監聽器,從不取消任何行為,鑑於瀏覽器仍會期待 preventDefault
的呼叫,也會對效能造成負面影響。
為了準確地告訴瀏覽器不必擔心(事件處理函式中)取消了預設行為,在 WHATWG 的 DOM 標準中存在著一個不太顯眼的特性(能解決這問題)。(它就是)Passive event listeners,瀏覽器對它的支援還是不錯的。事件監聽函式新接受一個可選的物件作為引數,告訴瀏覽器當事件觸發時,事件處理函式永遠不會取消預設行為。(當然,新增此引數後,)在事件處理函式中呼叫 preventDefault
將不再產生效果。
1 2 3 |
element.addEventListener('touchstart', e => { /* doSomething */ }, { passive: true }); |
針對不支援該引數的瀏覽器,這裡也有一個 polyfill 。這視訊清晰地展示了此改進帶來的影響。
舊技術執行良好,為何還要改動?
在現代網際網路中,過渡地依賴 JavaScript 在各瀏覽器上實現相同的互動效果不再是合理的,“跨瀏覽器相容性”已經成為過去式,更多的 CSS 屬性與 DOM API 方法正逐步被各大瀏覽器所支援。
在我們看來,當你的專案中,有特別酷炫的滾動效果時,漸進增強是最好的做法。
你應該提供(給使用者)所有(你能提供的)基礎使用者體驗,並逐步在更先進的瀏覽器上提供更好的體驗。
必要時使用 polyfill,它們不會產生(不必要的)依賴,一旦(某個 polyfill 所支援的屬性)得到廣泛地支援,你就可以輕鬆地將它刪掉。
六個月之前,在本文尚未成文之時,之前我們描述的屬性只被少量的瀏覽器所支援。而到了本文發表之時,這些屬性已被廣泛地支援。
也許到了現在,當你上下翻閱本文之時,(之前不支援某些屬性的)瀏覽器已經支援了該屬性,這使得你程式設計更容易,並使你的應用打包出來體積更小。
感謝閱讀至此!查閱瀏覽器的更新日誌,積極參與討論,有助於 web 標準駛向正確的方向。祝大家一帆風順,順利滑(滾)向未來!