現代前端滾動實現的各種解讀指南

原文鏈接: http://caibaojian.com/css-scroll.html

來自衆成翻譯的一篇:滑向未來(現代 JavaScript 與 CSS 滾動實現指南),裏面解釋了現代前端中基於web標準的改善,各種流暢的滾動CSS和JavaScript代碼,這些特性將使你的頁面更平滑、美觀。

一些(網站)滾動的效果是如此令人着迷但你卻不知該如何實現,本文將爲你揭開它們的神祕面紗。我們將基於最新的技術與規範爲你介紹最新的 JavaScript 與 CSS 特性,(當你付諸實踐時,)將使你的頁面滾動更平滑、美觀且性能更好。 大多數的網頁的內容都無法在一屏內全部展現,因而(頁面)滾動對於用戶而言是必不可少的。對於前端工程師與 UX 設計師而言,跨瀏覽器提供良好的滾動體驗,同時符合設計(要求),無疑是一個挑戰。儘管 web 標準的發展速度遠超從前,但代碼的實現往往是落後的。下文將爲你介紹一些常見的關於滾動的案例,檢查一下你所用的解決方案是否被更優雅的方案所代替。

消逝的滾動條

在過去的三十年裏,滾動條的外觀不斷改變以符合設計的趨勢,設計師們爲(滾動條的)顏色、陰影、上下箭頭的形狀與邊框的圓角實驗了多種風格。以下是 Windows 上的變化歷程:

http://pic.caibaojian.com/uploads/2018/05/t0192ca4668e75fbabf.png

(Windows 上的滾動條) 在2011年,蘋果設計師從 ios 上獲得靈感,爲如何定義“美觀的”滾動條確定了方向。所有滾動條均從 Mac 電腦中消失,不再佔據任何頁面空間,只有在用戶觸發滾動時(滾動條)纔會重新出現(有些用戶會設置不隱藏滾動條)。

http://pic.caibaojian.com/uploads/2018/05/t012d0ddfb483382882.png

(Mac 上的滾動條) 滾動條安靜地消逝並未引起蘋果粉絲的不滿,已經習慣了 iPhone 與 iPad 上滾動方式的用戶很快地習慣了這一設計。大多數開發人員與設計師都認爲這是一個“好消息”,因爲計算滾動條的寬度可真是件苦差事。

然而,我們生活在一個擁有衆多操作系統與瀏覽器的世界中,它們(對於滾動)的實現各不相同。如果你和我們一樣是一名 Web 開發者,你可不能把“滾動條問題”置之不理。

以下將爲你介紹一些小技巧,使你的用戶在滾動時有更好的體驗。

隱藏但可滾動

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

body {
  overflow: hidden;
}

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

http://pic.caibaojian.com/uploads/2018/05/t011ad376d358ab2462.gif

(注意紅色剪頭) 在這個示例中,爲了演示目的,我們在 Mac 系統中設置了強制顯示滾動條,因而用戶體驗與 Windows 用戶相似。 我們該如何解決這個問題呢?如果我們知道滾動條的寬度,每次當模態框出現時,可在主頁面的右邊設置一點邊距。 由於不同的操作系統與瀏覽器對滾動條的寬度不一,因而獲取它的寬度並不容易。在Mac 系統中,無論任何瀏覽器(滾動條)都是統一15px,然而 Windows 系統可會令開發者發狂:

http://pic.caibaojian.com/uploads/2018/05/t011568ba7ae0bebd32.png

(“百花齊放”的寬度) 注意,以上僅是 Windows 系統下基於當前最新版瀏覽器(測試所得)的結果。以前的(瀏覽器)版本(寬度)可能有所不同,也沒人知道未來(滾動條的寬度)會如何變化。 不同於猜測(滾動條的寬度),你可以通過 JavaScript 計算它的寬度(譯者注:實測以下代碼僅能測出原始的寬度,通過 CSS 改變了滾動條寬度後,以下代碼也無法測出實際寬度):

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

html {
  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)。Edge

也有計劃實現它們

。但三年過去了,該計劃仍在中等優先級中(而尚未被實現)。 當我們討論滾動條的定製時,Mozilla 基金會基本上是無視了設計師的需求。(有開發者在)

17年前

就已經提出了一個希望修改滾動條樣式的請求。而就在幾個月前,Jeff Griffiths(Firefox 瀏覽器總監)終於爲這個問題作出了回答:

“除非團隊中有人對此有興趣,否則我對此毫不關心。”

公平地說,從 W3C 的角度看來,儘管 WebKit 的實現得到廣泛的支持,但它仍然不是標準。現有的爲滾動條修改樣式的草案,是基於 IE 的:僅能修改它的顏色。 伴隨着請求如同 WebKit 一樣支持滾動條樣式修改

issue

的提交,爭議仍在繼續。如果你想影響 CSS 工作小組,是時候參與討論了。也許這不是優先級最高的問題,但(如同 WebKit 一樣修改滾動條樣式)得到標準化後,能使很多前端工程師與設計師輕鬆很多。

流暢的操作體驗

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

id

即可:

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

點擊該鏈接會

到(該錨點對應的)區塊上,(然而) UX 設計師一般會堅持認爲該過程應是平滑地運動的。

GitHub 上有大量造好的輪子

(幫你解決這個問題),然而它們或多或少都用到 JavaScript。(其實)只用一行代碼也能實現同樣的效果,最近DOM API 中的

Element.scrollIntoView()

可以通過

傳入配置對象

來實現平滑滾動:

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

然而該屬性

兼容性較差

且仍是通過腳本(來控制樣式)。如有可能,應儘量少用額外的腳本。 幸運的是,有一個全新的

CSS 屬性

(仍在工作草案中),可以用簡單的一行代碼改變整個頁面滾動的行爲。

html {
  scroll-behavior: smooth;
}

結果如下:

http://pic.caibaojian.com/uploads/2018/05/t013bea140bd875db38.gif

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

http://pic.caibaojian.com/uploads/2018/05/t017553ad00d8fac638.gif

(平滑地滾動) 你可以在

codepen

上試驗這個屬性。在撰寫本文時,

scroll-behavior

僅在 Chrome、 Firefox 與 Opera 上被支持,但我們希望它能被廣泛支持,因爲使用 CSS (比使用 JavaScript)在解決頁面滾動問題時優雅得多,並更符合“

漸進增強

”的模式。

粘性 CSS

另一個常見的需求是根據滾動方向動態地定住元素,即有名的“粘性(即 CSS 中的

position: sticky

)”效應。

http://pic.caibaojian.com/uploads/2018/05/t01b63092e8592f0126.gif

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

Element.getBoundingClientRect()

]時(

https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)。%E3%80%82)

不久之前,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 */
}));

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

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

爲了使(節流後的)滾動更平滑,你可以通過使用

window.requestAnimationFrame()

來實現函數節流:

function 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);

(點擊

這裏

,查看觸發迴流的 DOM 屬性和方法。) 此 API 被廣泛地支持,但仍有一些瀏覽器需要

polyfill

。儘管如此,它仍是目前最好的解決方案。

滾動邊界問題

如果你的彈框或下拉列表是可滾動的,那你務必要了解

連鎖滾動

相關的問題:當用戶滾動到(彈框或下拉列表)末尾(後再繼續滾動時),整個頁面都會開始滾動。 (連鎖滾動的表現) 當滾動元素到達底部時,你可以通過(改變)頁面的

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 });

針對不支持該參數的瀏覽器,這裏也有一個

polyfill

這視頻

清晰地展示了此改進帶來的影響。

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

在現代互聯網中,過渡地依賴 JavaScript 在各瀏覽器上實現相同的交互效果不再是合理的,“跨瀏覽器兼容性”已經成爲過去式,更多的 CSS 屬性與 DOM API 方法正逐步被各大瀏覽器所支持。 在我們看來,當你的項目中,有特別酷炫的滾動效果時,

漸進增強

是最好的做法。

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

必要時使用 polyfill,它們不會產生(不必要的)依賴,一旦(某個 polyfill 所支持的屬性)得到廣泛地支持,你就可以輕鬆地將它刪掉。 六個月之前,在本文尚未成文之時,之前我們描述的屬性只被少量的瀏覽器所支持。而到了本文發表之時,這些屬性已被廣泛地支持。 也許到了現在,當你上下翻閱本文之時,(之前不支持某些屬性的)瀏覽器已經支持了該屬性,這使得你編程更容易,並使你的應用打包出來體積更小。


感謝閱讀至此!查閱瀏覽器的更新日誌,積極參與討論,有助於 web 標準駛向正確的方向。祝大家一帆風順,順利滑(滾)向未來! 譯文:

https://www.zcfy.cc/article/scroll-to-the-future

原文鏈接:

evilmartians.com

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