解析移動端滾動穿透

滾動穿透在移動端開發中是一個很常見的問題,產生詭異的交互行爲,影響用戶體驗,同時也讓咱們的產品看起來不那麼「專業」。雖然很多產品選擇容忍了這樣的行爲,可是做爲追求極致的工程師,應該去了解爲何會產生以及如何去解決。javascript

什麼是滾動穿透

移動端開發中避免不了會在頁面上進行彈窗、加浮層等這種操做。一個最多見的場景就是整個頁面上有一個遮罩層,上面畫着各類各樣的東西,具體是什麼就不討論。實現這樣一個遮罩層可難不住即便是一個剛開始寫前端的小白。可是這裏有一個問題就是若是不對遮罩層作任何處理,當用戶在上面滑動時會發現遮罩層下方的頁面竟然也在滾動,這就很 interesting 了。就以下面的例子,一個名爲mask長寬都是屏幕大小的遮罩層,咱們在上面滑動時,下面的內容也在跟隨滾動,即滾動「穿透」到了下方,這就是滾動穿透(scroll-chaining)。css

scroll-chaining

上方 demo 的遮罩層底部是一個逐漸變藍的內容容器,可是滑動上面遮罩層時,底部也跟隨滾動了,這只是一個最簡單的場景,後面咱們會討論更復雜的狀況。html

爲何會出現

目前 Google 上搜滾動穿透會出現一大堆教你如何解決的文章,可是它們都是在告訴你怎麼解決怎麼 hack 掉這種交互異常。並無告訴讀者爲何會產生這種行爲,甚至認爲這是瀏覽器的一個 bug。對於我來講這個是難以理解的,由於就算解決了問題,其實也並不知道問題的根本是怎樣的。前端

認知誤區

有一個誤區就是咱們設置了一個和屏幕同樣大小的遮罩層,蓋住了下面的內容,按理說咱們應該能屏蔽掉下方的全部事件也就是說不可能觸發下面內容的滾動。那麼咱們就去看一下規範,何時會觸發滾動。java

// https://www.w3.org/TR/2016/WD...
When asked to run the scroll steps for a Document doc, run these steps:git

  1. For each item target in doc’s pending scroll event targets, in the order they were added to the list, run these substeps:
  • If target is a Document, fire an event named scroll that bubbles at target.
  • Otherwise, fire an event named scroll at target.
  1. Empty doc’s pending scroll event targets.

經過規範咱們能夠明白的 2 點是,首先滾動的 target 能夠是 document 和裏面的 element。其次,在 element 上的 scroll 事件是不冒泡的,document 上的 scroll 事件冒泡。github

因此若是咱們想經過在 scroll 的節點上去阻止它的滾動事件冒泡來解決問題是不可行的!由於它根本就不冒泡,沒法觸及 dom tree 的父節點何談觸發它們的滾動。web

那麼問題是怎麼產生的呢,其實規範只說明瞭瀏覽器應該在何時滾動,而沒有說不該該在何時滾動。瀏覽器正確實現了規範,滾動穿透也並非瀏覽器的 bug。咱們在頁面上加了一個遮罩層並不會影響 document 滾動事件的產生。根據規範,若是目標節點是不能滾動的那麼將會嘗試 document 上的滾動,也就是說遮罩層雖然不可滾動,可是這個時候瀏覽器會去觸發 document 的滾動從而致使了下方文檔的滾動。也就是說若是 document 也不可滾動了,也就不會有這個問題了。這就引出瞭解決問題的第一種方案:把 document 設置爲 overflow hidden。瀏覽器

怎麼解決

overflow hidden

既然滾動是因爲文檔超出了一屏產生的,那麼就讓它超出部分 hidden 掉就行了,因此在遮罩層被彈出的時候能夠給 html 和 body 標籤設置一個 class:app

.modal--open {
  height: 100%;
  overflow: hidden;
}

這樣文檔高度和屏幕同樣,天然不會存在滾動了。可是這樣又會引來一個新的問題,若是文檔以前存在必定的滾動高度那麼這樣設置後會致使以前的滾動距離失效,文檔滾回了最頂部,這樣一來豈不是得不償失?可是咱們能夠在加 class 以前記錄好以前的滾動具體而後在關閉遮罩層的時候把滾動距離設置回來。這樣問題是能夠獲得解決的實現成本也很低,可是若是遮罩層是透明的,彈出後用戶仍然會看到丟失距離後的下方頁面,顯然這樣並非完美的方案。

prevent touch event

還有一種辦法就是咱們直接阻止掉遮罩層和彈窗的 touch event 這樣就不會在移動端觸發 scroll 事件了。可是在 PC 上沒有 touch 事件, scroll 事件仍然能夠被觸發,緣由上面咱們也說過,scroll 事件是滾動它能滾動的元素。這裏咱們解決的是移動端的問題,例子以下:

scroll-chaining

<div id="app">
  <div class="mask">mask</div>
  <div class="dialog">dialog</div>
</div>
const $mask = document.querySelector(".mask");
const $dialog = document.querySelector(".dialog");
const preventTouchMove = $el => {
  $el.addEventListener(
    "touchmove",
    e => {
      e.preventDefault();
    },
    { passive: false }
  );
};
preventTouchMove($mask);
preventTouchMove($dialog);

上面咱們經過 prevent touchmove 來阻止頁面的觸摸事件從而禁止進一步的頁面滾動,在 addEventListener 最後一個參數咱們將 passive 顯示的設置爲 false,這裏是有用意的。關於 passive event listener 這裏又是一個話題咱們就不展開說了,就是瀏覽器爲了優化滾動性能作的一些改進,具體能夠看 網站使用被動事件偵聽器以提高滾動性能,因爲在 Chrome 56 開始將會默認開啓 passive event listener 因此不能直接在 touch 事件中使用 preventDefault,須要先將 passive 選項設置爲 false 才行。

這裏咱們解決了在頁面上普通彈窗的問題,可是若是 dialog 的內容是能夠滾動的,這樣將其阻止了 touch 事件將會致使其內容也不能正常滾動,因此還有要進一步優化才行。

進一步優化

如今的場景是咱們的彈窗是能夠滾動的,因此不能再直接將其 touch 事件阻止,去掉後咱們發現會產生新的問題。遮罩層被阻止了 touch 事件不能使下方滾動,可是彈出層 modal 這裏內容是可滾動的,在 touch modal 時能正常滾動裏面的內容。可是 modal 滾動到最上方或者最下方時仍然能觸發 document 的滾動,效果以下:

scroll-chainging

咱們看到當 modal 滾動在頂部時仍然能拖動下方 document。這樣咱們只能監聽用戶手勢,若是 modal 已經滑動到了底部或者頂部且還要往上或者下滑動則也要 prevent modal 的 touch 事件。簡單實現一個 fuckScrollChaining 函數:

function fuckScrollChaining($mask, $modal) {
  const listenerOpts = { passive: false };
  $mask.addEventListener(
    "touchmove",
    e => {
      e.preventDefault();
    },
    listenerOpts
  );
  const modalHeight = $modal.clientHeight;
  const modalScrollHeight = $modal.scrollHeight;
  let startY = 0;

  $modal.addEventListener("touchstart", e => {
    startY = e.touches[0].pageY;
  });
  $modal.addEventListener(
    "touchmove",
    e => {
      let endY = e.touches[0].pageY;
      let delta = endY - startY;

      if (
        ($modal.scrollTop === 0 && delta > 0) ||
        ($modal.scrollTop + modalHeight === modalScrollHeight && delta < 0)
      ) {
        e.preventDefault();
      }
    },
    listenerOpts
  );
}

完整實如今 這裏,至此不管彈出層內容是否可滾動都不會致使下方 document 跟隨滾動。

原文出處歡迎討論 https://github.com/Jiavan/blo...
相關文章
相關標籤/搜索