現代 JavaScript 與 CSS 令人着迷滾動實現指南

一些(網站)滾動的效果是如此令人着迷但你卻不知該如何實現,本文將爲你揭開它們的神祕面紗。我們將基於最新的技術與規範爲你介紹最新的 JavaScript 與 CSS 特性,(當你付諸實踐時,)將使你的頁面滾動更平滑、美觀且性能更好。

大多數的網頁的內容都無法在一屏內全部展現,因而(頁面)滾動對於用戶而言是必不可少的。對於前端工程師與 UX 設計師而言,跨瀏覽器提供良好的滾動體驗,同時符合設計(要求),無疑是一個挑戰。儘管 web 標準的發展速度遠超從前,但代碼的實現往往是落後的。下文將爲你介紹一些常見的關於滾動的案例,檢查一下你所用的解決方案是否被更優雅的方案所代替。

這裏推薦一下我的學習交流q=u=n=:731771211,裏面都是學習前端的,如果你想製作酷炫的網頁,想學習編程。從最基礎的HTML+CSS+JS【炫酷特效,遊戲,插件封裝,設計模式】到移動端HTML5的項目實戰的學習資料都有整理,送給每一位前端小夥伴,有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入。

點擊:加入

隱藏但可滾動

先來看看一個關於模態框的經典例子。當它被打開的時候,主頁面應該停止滾動。在 CSS 中有如下的快捷實現方式:

body {
  overflow: hidden;
}

但上述代碼會帶來一點不良的副作用:

現代 JavaScript 與 CSS 令人着迷滾動實現指南

在這個示例中,爲了演示目的,我們在 Mac 系統中設置了強制顯示滾動條,因而用戶體驗與 Windows 用戶相似。

我們該如何解決這個問題呢?如果我們知道滾動條的寬度,每次當模態框出現時,可在主頁面的右邊設置一點邊距。

由於不同的操作系統與瀏覽器對滾動條的寬度不一,因而獲取它的寬度並不容易。在Mac 系統中,無論任何瀏覽器(滾動條)都是統一15px,然而 Windows 系統可會令開發者發狂:

現代 JavaScript 與 CSS 令人着迷滾動實現指南

注意,以上僅是 Windows 系統下基於當前最新版瀏覽器(測試所得)的結果。以前的(瀏覽器)版本(寬度)可能有所不同,也沒人知道未來(滾動條的寬度)會如何變化。

不同於猜測(滾動條的寬度),你可以通過 JavaScript 計算它的寬度(譯者注:實測以下代碼僅能測出原始的寬度,通過 CSS 改變了滾動條寬度後,以下代碼也無法測出實際寬度):

onst 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 實現:

tml {
  overflow-y: scroll;
}

儘管“模態框抖動”問題解決了,但整體的外觀卻被一個無法使用的滾動條影響了,這無疑是設計中的硬傷。

在我們看來,更好的解決方案是完全地隱藏滾動條。純粹用 CSS 也是可以實現的。該方法(達到的效果)和 macOS 的表現並不是完全一致,(當用戶)滾動時滾動條仍然是不可見的。滾動條總是處於不可見狀態,然而頁面是可被滾動的。對於Chrome,Safari 和 Opera 而言,可以使用以下的 CSS:

.container::-webkit-scrollbar {
  display: none;
}

IE 或 Edge 可用以下代碼:

.container {
  -ms-overflow-style: none;
}

至於 Firefox,很不幸,沒有任何辦法隱藏滾動條。

正如你所見,並沒有任何銀彈。任何解決方案都有它的優點與缺點,應根據你項目的需要選擇最合適的。

外觀爭議

需要承認的是,滾動條的樣子在部分操作系統上並不好看。一些設計師喜歡完全掌控他們(所設計)應用的樣式,任何一絲細節也不放過。在 GitHub 上有上百個庫藉助 JavaScript 取代系統滾動條的默認實現,以達到自定義的效果。

但如果你想根據現有的瀏覽器定製一個滾動條呢?(很遺憾,)並沒有通用的 API,每個瀏覽器都有其獨特的代碼實現。

儘管5.5版本以後的 IE 瀏覽器允許你修改滾動條的樣式,但它只允許你修改滾動條的顏色。以下是如何重新繪製(滾動條)拖動部分與箭頭的代碼:

body {
  scrollbar-face-color: blue;
}

但只改變顏色對提高用戶體驗而言幫助不大。據此,WebKit 的開發者在2009年提出了(修改滾動條)樣式的方案。以下是使用 -webkit 前綴在支持相關樣式的瀏覽器中模擬 macOS 滾動條樣式的代碼:

::-webkit-scrollbar {
  width: 8px;
}
::-webkit-scrollbar-thumb {
  background-color: #c1c1c1;
  border-radius: 4px;
}

Chrome、Safari、Opera 甚至於 UC 瀏覽器或者三星自帶的桌面瀏覽器都支持(上述 CSS)。

流暢的操作體驗

對於滾動而言,最常見的任務是登錄頁的導航(跳轉)。通常,它是通過錨點鏈接來完成的。只需要知道元素的 id 即可:

<a href="#section">Section</a>

點擊該鏈接會 跳 到(該錨點對應的)區塊上,(然而) UX 設計師一般會堅持認爲該過程應是平滑地運動的。GitHub 上有大量造好的輪子(幫你解決這個問題),然而它們或多或少都用到 JavaScript。(其實)只用一行代碼也能實現同樣的效果,最近DOM API 中的 Element.scrollIntoView() 可以通過傳入配置對象來實現平滑滾動:

elem.scrollIntoView({
  behavior: 'smooth'
});

然而該屬性兼容性較差且仍是通過腳本(來控制樣式)。如有可能,應儘量少用額外的腳本。

幸運的是,有一個全新的 CSS 屬性(仍在工作草案中),可以用簡單的一行代碼改變整個頁面滾動的行爲。

html {
  scroll-behavior: smooth;
}

結果如下:

現代 JavaScript 與 CSS 令人着迷滾動實現指南

(從一個區塊跳到另一個)

現代 JavaScript 與 CSS 令人着迷滾動實現指南

(平滑地滾動)

你可以在 codepen 上試驗這個屬性。在撰寫本文時,scroll-behavior 僅在 Chrome、 Firefox 與 Opera 上被支持,但我們希望它能被廣泛支持,因爲使用 CSS (比使用 JavaScript)在解決頁面滾動問題時優雅得多,並更符合“漸進增強”的模式。

粘性 CSS

另一個常見的需求是根據滾動方向動態地定住元素,即有名的“粘性(即 CSS 中的position: sticky)”效應。

現代 JavaScript 與 CSS 令人着迷滾動實現指南

(一個粘性元素)

在以前的日子裏,要實現一個“粘性”元素需要編寫複雜的滾動處理函數去計算元素的大小。(然而)該函數較難處理元素在“黏住”與“不黏住”之間微小的延遲,(通常會)導致(元素)抖動的出現。通過 JavaScript 來實行(“粘性”元素)也有性能上的問題,特別是在(需要)調用 [Element.getBoundingClientRect() ]時

不久之前,CSS 實現了 position: sticky 屬性。只需通過指定(某方向上的)偏移量即可實現我們想要的效果。

.element {
  position: sticky;
  top: 50px;
}

(編寫上述代碼後,)剩下的就交由瀏覽器實現即可。你可以在 codepen 上試驗一下。撰寫本文之時,position: sticky 在各式瀏覽器(包括移動端瀏覽器)上支持良好,所以如果你還在使用 JavaScript 去解決這個問題的話,是時候換成純 CSS 的實現了。

全面使用函數節流

從瀏覽器的角度看來,滾動是一個事件,因此在 JavaScript 中是使用一個標準化的事件監聽器 addEventListener 去處理它: ,

window.addEventListener('scroll', () => {
  const scrollTop = window.scrollY;
  /* doSomething with scrollTop */
});

用戶往往高頻率地滾動(頁面),但如果滾動事件觸發太頻繁的話,會導致性能上的問題,可以通過使用函數節流這一技巧去優化它。

window.addEventListener('scroll', throttle(() => {
  const scrollTop = window.scrollY;
  /* doSomething with scrollTop */
}));

你需要定義一個節流函數包裝原來的事件監聽函數,(節流函數是)減少被包裝函數的執行次數,只允許它在固定的時間間隔之內執行一次:

ction throttle(action, wait = 1000) {
  let time = Date.now();
  return function() {
    if ((time + wait - Date.now()) < 0) {
        action();
        time = Date.now();
    }
  }
}

爲了使(節流後的)滾動更平滑,你可以通過使用 window.requestAnimationFrame() 來實現函數節流:

ion throttle(action) {
  let isRunning = false;
  return function() {
    if (isRunning) return;
    isRunning = true;
    window.requestAnimationFrame(() => {
      action();
      isRunning = false;
    });
  }
}

當然,你可以通過現有的開源輪子來實現,就像 Lodash 一樣。你可以訪問 codepen 來看看上述解決方案與 Lodash 中的 _.throttle 之間的區別。

使用哪個(開源庫)並不重要,重要的是在需要的時候,記得優化你(頁面中的)滾動處理函數。

在視窗中顯示

當你需要實現圖片懶加載或者無限滾動時,需要確定元素是否出現在視窗中。這可以在事件監聽器中處理,最常見的解決方案是使用 lement.getBoundingClientRect() :

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 來解決問題。它允許你追蹤目標元素與其祖先元素或視窗的交叉狀態。此外,儘管只有一部分元素出現在視窗中,哪怕只有一像素,也可以選擇觸發回調函數:

const observer = new IntersectionObserver(callback, options);

observer.observe(element);

滾動邊界問題

如果你的彈框或下拉列表是可滾動的,那你務必要了解連鎖滾動相關的問題:當用戶滾動到(彈框或下拉列表)末尾(後再繼續滾動時),整個頁面都會開始滾動。

現代 JavaScript 與 CSS 令人着迷滾動實現指南

(連鎖滾動的表現)

當滾動元素到達底部時,你可以通過(改變)頁面的 overflow 屬性或在滾動元素的滾動事件處理函數中取消默認行爲來解決這問題。

如果你選擇使用 JavaScript (來處理),請記住要處理的不是“scroll(事件)”,而是每當用戶使用鼠標滾輪或觸摸板時觸發的“wheel(事件)”:

function handleOverscroll(event) {
  const delta = -event.deltaY;
  if (delta < 0 && elem.offsetHeight - delta > 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 中可以通過以下一行代碼來解決:

.element {
  overscroll-behavior: contain;
}

公平地說,IE 與 Edge 實現了(它獨有的) -ms-scroll-chaining 屬性來控制連鎖滾動,但它並不能處理所有的情況。幸運的是,根據這消息,微軟的瀏覽器已經準備實現 overscroll-behavior 這一屬性了。

觸屏之後

觸屏設備上的滾動(體驗)是一個很大的話題,深入討論需要另開一篇文章。然而,由於很多開發者忽略了這方面的內容,這裏需要提及一下。

(滾動手勢無處不在,令人沉迷,以至於想出瞭如此瘋狂的主意去解決“滾動上癮”的問題。)

周圍的人在智能手機屏幕上上下移動他們的手指的頻率是多少呢?經常這樣對吧,當你閱讀本文時,你很可能就在這麼做。

當你的手指在屏幕上移動時,你期待的是:頁面內容平滑且流暢地移動。

蘋果公司開創了“慣性”滾動並擁有它的專利 。它訊速地成爲了用戶交互的標準並且我們對此已習以爲常。

但你也許已經注意到了,儘管移動端系統會爲你實現頁面上的慣性滾動,但當頁面內某個元素髮生滾動時,即使用戶同樣期待慣性滾動,但它並不會出現,這令人沮喪。

這裏有一個 CSS 的解決方案,但看起來更像是個 hack:

.element {
  -webkit-overflow-scrolling: touch;
}

爲什麼這是個 hack 呢?首先,它只能在支持(webkit)前綴的瀏覽器上才能工作。其次,它只適用於觸屏設備。最後,如果瀏覽器不支持的話,你就這樣置之不理嗎?但無論如何,這總歸是一個解決方案,你可以試着使用它。

在觸屏設備上,另一個需要考慮的問題是開發者如何處理 touchstart 與 touchmove 事件觸發時可能存在的性能問題,它對用戶滾動體驗的影響非常大。這裏詳細描述了整個問題。簡單來說,現代的瀏覽器雖然知道如何使得滾動變得平滑,但爲確認(滾動)事件處理函數中是否執行了 Event.preventDefault() 以取消默認行爲,有時仍可能需要花費500毫秒來等待事件處理函數執行完畢。

即使是一個空的事件監聽器,從不取消任何行爲,鑑於瀏覽器仍會期待 preventDefault 的調用,也會對性能造成負面影響。

爲了準確地告訴瀏覽器不必擔心(事件處理函數中)取消了默認行爲,在 WHATWG 的 DOM 標準中存在着一個不太顯眼的特性(能解決這問題)。(它就是)Passive event listeners,瀏覽器對它的支持還是不錯的。事件監聽函數新接受一個可選的對象作爲參數,告訴瀏覽器當事件觸發時,事件處理函數永遠不會取消默認行爲。(當然,添加此參數後,)在事件處理函數中調用 preventDefault 將不再產生效果。

element.addEventListener('touchstart', e => {
  /* doSomething */
}, { passive: true });

舊技術運行良好,爲何還要改動?

在現代互聯網中,過渡地依賴 JavaScript 在各瀏覽器上實現相同的交互效果不再是合理的,“跨瀏覽器兼容性”已經成爲過去式,更多的 CSS 屬性與 DOM API 方法正逐步被各大瀏覽器所支持。

在我們看來,當你的項目中,有特別酷炫的滾動效果時,漸進增強是最好的做法。

你應該提供(給用戶)所有(你能提供的)基礎用戶體驗,並逐步在更先進的瀏覽器上提供更好的體驗。

必要時使用 polyfill,它們不會產生(不必要的)依賴,一旦(某個 polyfill 所支持的屬性)得到廣泛地支持,你就可以輕鬆地將它刪掉。

六個月之前,在本文尚未成文之時,之前我們描述的屬性只被少量的瀏覽器所支持。而到了本文發表之時,這些屬性已被廣泛地支持。

也許到了現在,當你上下翻閱本文之時,(之前不支持某些屬性的)瀏覽器已經支持了該屬性,這使得你編程更容易,並使你的應用打包出來體積更小

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章