滾動穿透的6種解決方案【已自測】

 

在移動端中,若是咱們使用了一個固定定位的遮罩層,且其下方的dom結構的寬度|高度超出屏幕的寬度|高度,那麼即便遮罩層彈出後鋪滿了整個屏幕,其下方的dom結構依然能夠滾動,這就是你們所說的「滾動穿透」。javascript

並且常常是你在pc模擬器上沒有問題,可是真機打開就必定會出現。css

 

這個經典八阿哥也是面試時常常會被追問的問題。相信能看到這篇文章的你,已是遇到了這個問題。我就不gif展現問題效果了。java

 

接下來我網羅了網絡,整理了別人說的方案和我本身的方案,一共實現了六種方法,並通過了本身手機自測。git

 

各方法操做難易不一樣,分別針對彈層和body是否超出一屏可滾動等不一樣狀況。看官能夠對症下藥。github

 

贈送一套自定義手勢滾動效果的代碼哦~web

 

1、body無滾動 + 彈層無滾動[css-超出隱藏]

 

適用場景需知足如下條件:面試

    一、body最好是一屏、無滾動微信

    二、雖然body內容超出一屏需滾動,但觸發彈層出現的按鈕在第一屏中網絡

    三、彈層不用滾動效果dom

 

解決方案:

彈層出現時,用css給body設置固定定位和超出隱藏。

 

關鍵代碼:

 

btn.onclick = function () {
      // 彈層出現
      layer.style.display = 'block';
      document.body.style.overflow = 'hidden';
      document.body.style.position = 'fixed';//果真是由於加了fixed,就會自動回滾到頂部
    }
    var closeBtn = document.getElementById('close');
    closeBtn.onclick = function () {
      // 彈層關閉
      layer.style.display = 'none';
      document.body.style.overflow = 'auto';
      document.body.style.position = 'static';
    }

 

ps:我偷懶直接js控制了行間樣式,但標準寫法應該是給body添加類名來控制

 

侷限問題:
body滾動後再觸發彈層,會使body頁面回滾到頂部。

 

贅述:

這個方案是簡單粗暴的給body設置:

body {

    overflow: hidden;

    position: fixed;

}

 

起初,我只給body一個overflow隱藏,彈窗出現後上下滑動,底部的body也不會滑動,瞬間感受世界很美好。

 

可是晴天霹靂來的太快,在模擬器是起做用的,可是到了真機上,body仍是會滾動。因此必須添加上fixed固定定位,才能在彈窗出現後,body不能被拖動。

 

可是,也由於加了position: fixed;出現了新問題:

它會致使觸發彈層後,body回滾、定位到頂部。假如用戶向下翻頁了幾屏後,再觸發彈層,整個頁面就會回滾到最初的頂部,這對用戶體驗來講是很是很差的。

 

所以,這種方案的適用環境也就很是侷限,只能適用觸發彈層出現的按鈕位於第一屏中的狀況。須要咱們能確保用戶在不發生上滑頁面滾動屏幕的狀況下就能觸發彈層出現,就不會出現我上邊說的問題。

 

或者乾脆咱們就是一個swiper項目,每一頁都是一屏,body不能滾動,那麼在項目中用這個方法,仍是性價比很高的。

 

 

 

2、body無滾動 + 彈層內部滾動[css-彈框超出滾動|真機有bug]

 

適用場景需知足如下條件:

    一、body最好是一屏、無滾動

    二、雖然body內容超出一屏需滾動,但觸發彈層出現的按鈕在第一屏中

 

解決方案:

彈層出現時,用css給body設置固定定位和超出隱藏。

至於彈層內部的滾動,設置一個overflow: scroll;便可。

不過爲了流暢體驗,能夠加上-webkit-overflow-scrolling: touch,以解決在IOS上滾動慣性失效的問題,提升滾動的流暢度。

 

關鍵代碼:

JS控制彈窗的交互、body的禁止滾動

btn.onclick = function () {
      // 彈層出現
      layer.style.display = 'block';
      document.body.style.overflow = 'hidden';
      document.body.style.position = 'fixed';//果真是由於加了fixed,就會自動回滾到頂部
    }
    var closeBtn = document.getElementById('close');
    closeBtn.onclick = function () {
      // 彈層關閉
      layer.style.display = 'none';
      document.body.style.overflow = 'auto';
      document.body.style.position = 'static';
    }

 

css添加彈層的超出滾動效果

1 overflow-y: scroll; 2 -webkit-overflow-scrolling: touch;/* 解決在IOS上滾動慣性失效的問題 */

 

侷限問題:

彈層中內容滾動到頂部或底部後,還會連帶頁面body一塊兒滾動。也就是還會發生穿透效果。

 

贅述:

第一條中,咱們只是在彈窗打開的時候,簡單的禁止了body的滾動效果。可是限制條件是,咱們的彈窗也不能滾動。此次,咱們優化一下 -- 容許彈窗內部滾動。

 

在前邊代碼的基礎上,經過css單純的設置一下縱軸的超出滾動。

overflow-y: scroll;

只有這一句滾動效果不太好,沒有原生滾動流暢。加一個屬性

-webkit-overflow-scrolling: touch;/* 解決在IOS上滾動慣性失效的問題 */

 

可是這只是簡單地解決了一個問題:實現了滑動彈窗其餘地方(蒙層背景),底部body頁面確實未跟隨滾動。

 

真正的問題是當咱們滑動彈窗可滾動區域,把可滾動區域的內容上滑到底部或下拉到頂部後,再觸發彈窗可滾動區域準備滑動,此時的背景頁面就會跟隨滾動。真是恐怖。

 

所以還須要咱們對彈層的可滾動區域的滑動事件作監聽:

第一種狀況,若向上滑動時,到達底部;或者第二種狀況,若向下滑動時,已到頂部。

這兩種狀況任意一種發生時,就阻止滑動事件。

這段邏輯代碼以下:

var targetY = null;
layerBox.addEventListener('touchstart', function (e) {
  //clientY-客戶區座標Y 、pageY-頁面座標Y
  targetY = Math.floor(e.targetTouches[0].clientY);
});
layerBox.addEventListener('touchmove', function (e) {
  // 檢測可滾動區域的滾動事件,若是滑到了頂部或底部,阻止默認事件
  var NewTargetY = Math.floor(e.targetTouches[0].clientY),//本次移動時鼠標的位置,用於計算
    sTop = layerBox.scrollTop,//當前滾動的距離
    sH = layerBox.scrollHeight,//可滾動區域的高度
    lyBoxH = layerBox.clientHeight;//可視區域的高度
  if (sTop <= 0 && NewTargetY - targetY > 0 && '鼠標方向向下-到頂') {
    // console.log('條件1成立:下拉頁面到頂');
    e.preventDefault();
  } else if (sTop >= sH - lyBoxH && NewTargetY - targetY < 0 &&
    '鼠標方向向上-到底') {
    // console.log('條件2成立:上翻頁面到底');
    e.preventDefault();
  }
}, false);

 

3、body滾動 + 彈層無滾動[js-阻止彈層中touchmove的默認行爲]

 

適用場景:

  一、(適用)body可滾動

  二、(適用)觸發彈層出現的按鈕能夠在任意位置



需知足如下條件:

    一、(需知足)彈層內容不須要滾動

 

解決方案:

當彈層出現的時候不須要再禁掉body的滾動效果了,咱們能夠從彈層方面入手,阻止彈框的touchmove事件的默認行爲。就能阻止滾動穿透。

 

關鍵代碼:

js控制彈窗的交互、彈窗的禁止滾動

 

btn.onclick = function () {
  layer.style.display = 'block';
  layer.addEventListener('touchmove',function(e){
    e.preventDefault();
  },false);
}
var closeBtn = document.getElementById('close');
closeBtn.onclick = function () {
  layer.style.display = 'none';
    // 彈窗關閉後,可解除全部禁止 - 懶人就不寫了
}

 

 

侷限問題:

由於touchmove被禁掉了,就會形成彈窗內部全部位置都不能響應touchmove事件,效果上就是彈窗內部不能再滾動了。

 

贅述:

在彈層不須要超出滾動的狀況下,纔可使用這個。也就是禁止整個彈窗的touchmove的默認事件,以阻止滾動穿透。

 

一樣,若是彈層中須要滾動效果,則不能解決了。那麼這時,就引來咱們的主題難點,能夠有如下幾種思路解決:

 

 

 

4、body滾動 + 彈層內部滾動[js-檢測touchmove的target]

簡單粗暴,一針見血:誰能動誰動,誰不能動就禁止touchmove事件的preventEvent默認行爲。

 

適用如下場景:

    一、body可滾動

    二、觸發彈層出現的按鈕能夠在任意位置

    三、彈層能夠滾動

簡單來講,就是適用任何場景

 

解決方案:

檢測touchmove事件,若是touch的目標是彈窗不可滾動區域(背景蒙層)就禁掉默認事件,反之就不作控制。

 

可是一樣的問題是,須要判斷滾動到頂部和滾動到底部的時候禁止滾動。不然,就和第二條同樣,觸碰到上下兩端,彈窗可滾動區域的滾動條到了頂部或者底部,依舊穿透到body,使得body跟隨彈窗滾動。

 

因此依舊須要一樣的代碼,對可滾動區域的touchmove作監聽:若到頂或到底,一樣阻止默認事件。

 

須要作的事情有:

一、預存一個全局變量targetY

 

二、監聽可滾動區域的touchstart事件,記錄下第一次按下時的

e.targetTouches[0].clientY值,賦值給targetY

 

三、後期touchmove裏邊獲取每次的e.targetTouches[0].clientY與第一次的進行比較,能夠得出用戶是上滑仍是下滑手勢。

 

四、若是手勢是向上滑,且頁面如今滾動的位置恰好是整個可滾動高度——彈窗內容可視區域高度的值,說明上滑到底,阻止默認事件。

同理,若是手勢是向下滑,而且當前滾動高度爲0說明當前展現的已經在可滾動內容的頂部了,此時再次阻止默認事件便可。

 

兩個判斷條件能夠寫到一個if中,用 || (或)表示便可。我這裏爲了代碼可讀性,分開寫了:

if (sTop <= 0 && NewTargetY - targetY > 0 && '鼠標方向向下-到頂') {
      // console.log('條件1成立:下拉頁面到頂');
      e.preventDefault();
    } else if (sTop >= sH - lyBoxH && NewTargetY - targetY < 0 &&
      '鼠標方向向上-到底') {
      // console.log('條件2成立:上翻頁面到底');
      e.preventDefault();
    }

 

 

完整代碼:

出現彈窗時:

btn.onclick = function () {
  layer.style.display = 'block';
  layer.addEventListener('touchmove', function (e) {
    e.stopPropagation();
    if (e.target == layer) {
      // 讓不能夠滾動的區域不要滾動
      console.log(e.target, '我就是一個天才!!!');
      e.preventDefault();
    }
  }, false);
  var targetY = null;
  layerBox.addEventListener('touchstart', function (e) {
    //clientY-客戶區座標Y 、pageY-頁面座標Y
    targetY = Math.floor(e.targetTouches[0].clientY);
  });
  layerBox.addEventListener('touchmove', function (e) {
    // 檢測可滾動區域的滾動事件,若是滑到了頂部或底部,阻止默認事件
    var NewTargetY = Math.floor(e.targetTouches[0].clientY),//本次移動時鼠標的位置,用於計算
      sTop = layerBox.scrollTop,//當前滾動的距離
      sH = layerBox.scrollHeight,//可滾動區域的高度
      lyBoxH = layerBox.clientHeight;//可視區域的高度
    if (sTop <= 0 && NewTargetY - targetY > 0 && '鼠標方向向下-到頂') {
      // console.log('條件1成立:下拉頁面到頂');
      e.preventDefault();
    } else if (sTop >= sH - lyBoxH && NewTargetY - targetY < 0 &&
      '鼠標方向向上-到底') {
      // console.log('條件2成立:上翻頁面到底');
      e.preventDefault();
    }
  }, false);
}

 

隱藏彈窗時:

var closeBtn = document.getElementById('close');
closeBtn.onclick = function () {
  layer.style.display = 'none';
    // 彈窗關閉後,可解除全部禁止 - 懶人就不寫了
}

 

 

 

 

5、body滾動 + 彈層內部滾動[js-代碼模擬上下滑動手勢效果]

 

我想,既然咱們監控彈層、監控touchY那麼辛苦了已經,還差再辛苦一點,本身寫一個模擬手勢滾動效果嘛!

 

此次依舊從彈層上入手,不讓彈層用css自動的超出滾動,而是超出隱藏,而後簡單粗暴地利用JS的touchstart、touchmove、touchend等事件,手動寫一個自定義滾動效果。

 

適用場景:

一切,這種作法應用到項目中過,經得起測試的考驗。

 

解決方案與思路:

具體制做思路寫在js註釋上。

 

一、交互代碼

 

/* 交互代碼 */
btn.onclick = function () {
  layer.style.display = 'block';
  //爲了個人css能統一使用,這裏偷個懶,加個行間樣式,
  // 把以前作demo用的overflow滾動給禁掉,而後改了點別的樣式
  layerBox.style.overflow = 'hidden'; 
  layerBox.style.paddingTop = 0; 
  layerList.style.paddingTop = 0; 
  layerList.style.paddingBottom = 0; 
}
var closeBtn = document.getElementById('close');
closeBtn.onclick = function () {
  console.log('?/*  */');
  layer.style.display = 'none';
  // 彈窗關閉後,可解除全部禁止 - 懶人就不寫了
}

 

二、禁掉彈窗的touchmove 的默認事件

/* 禁掉全部的touchmove事件 */
layer.addEventListener('touchmove', function (e) {
  e.preventDefault();
}, false);

 

 

三、重寫手勢滑動效果

/* 從新寫touchmove效果 */
var targetY = null,
  transH = 0,
  lastY = 0;
layerBox.addEventListener('touchstart', function (e) {
  //這裏簡單的把整個layerBox的默認事件給禁止了,因此close的click事件就不起做用了。
  // 能夠把結構再改改把close挪出來。或者js把close繞開:
  if(e.target != closeBtn){
    e.preventDefault();
  }
  //clientY-客戶區座標Y 、pageY-頁面座標Y
  lastY = targetY = Math.floor(e.targetTouches[0].clientY);
});
layerBox.addEventListener('touchmove', function (e) {
  // 爲了寫這個,還得改動一下結構
  var NewTargetY = Math.floor(e.targetTouches[0].clientY), //本次移動時鼠標的位置,用於計算
    sTop = layerBox.scrollTop, //當前滾動的距離
    sH = layerBox.scrollHeight, //可滾動區域的高度
    lyBoxH = layerBox.clientHeight; //可視區域的高度
  if (NewTargetY - targetY > 0 && '鼠標方向向下滑-上翻效果') {
    transH += NewTargetY - lastY;// 先把此次鼠標滑動的距離計算出來,疊加給transH
    transH = transH >= 0 ? 0 : transH;//本來transH是負值,若是一直向上翻,就須要一直+正值,一旦正負相加抵消到>=0,說明翻到頂了,就直接賦值爲頂,再也不上翻。
  } else if (NewTargetY - targetY < 0 && '鼠標方向向上滑動-下拉效果') {
    transH -= lastY - NewTargetY;// 先把此次鼠標滑動的距離計算出來,疊減給transH
    transH = Math.abs(transH) > sH - lyBoxH ? -(sH - lyBoxH) : transH;//若是transH的絕對值大於可滾動的距離了,說明翻到底,則把可滾動區域翻到底的值賦給他。不然就一直下滾鼠標移動的距離
  }
  layerList.style.transform = `translateY(${ transH }px)`;
  lastY = NewTargetY;
}, false);

 

 

大體思路關鍵點就在touchmove裏邊:
一、在touchstart的時候,監聽用戶手勢按下,記錄初次按下的座標點y的值y1。

 

二、touchmove手勢移動的時候,再次獲取最新的座標點y的值y2,(其實記錄可滾動區域的可滾動高度、當前滾動距離等能夠在一開始就記錄,我這裏寫到了touchmove裏,還能夠再優化)。

 

三、而後經過計算y1和y2 的差值判斷出用戶是朝哪一個方向移動的手勢。

 

四、進而根據不一樣的手勢方向給彈層可滾動內容的transform添加位移translate效果(或者基礎用position: absolute,再根據手勢移動的距離,動態設置top的值。代碼不止一種)。思路就是把手勢移動的長度添加到彈層上下移動的距離上。

 

五、可能須要多考慮的一點是,當用戶一直上翻到底或者一直下拉到頂時,作一下極值的判斷和限制。

 

六、最後把本次移動到的點y2替換給y1,根據手勢移動實時更新當前手勢的地址。

 

七、另外這裏還能夠在touchend事件裏,把touchstart和touchmove包括自身touchend的事件都解綁掉。我偷懶就不寫了。

 

問題侷限:

很差的點就是沒有原生滾動條那種效果,一點也不靈動,只能鼠標移動多少、可滾動區域挪動多少。

 

 

 

6、body滾動 + 彈層內部滾動[css+js-記錄滾動位置]

 

換個腦子,回到最初 尋找新的思路。

 

不從彈層上入手,也就是不由掉彈層的touchmove默認事件。

而是繼續給body一個overflow: hidden;和position: fixed;就會有頁面跳轉到頂部的現象。

 

這時,咱們能夠經過記錄用戶打開彈窗前所滾動頁面的位置,在彈層展開的時候賦給body在css中的top值,等關閉彈層的時候,再把這個值賦值給body在js中的scrollTop值,還原body的滾動位置。

 

這種原理簡單,理解方便。而且各方面都能實現。好比說:

  • body能夠繼續滾動、彈層出來後他的top值限制他不會跳到頂部、

  • 彈層中無論短仍是長,需不須要滾動,都不care,自由活動、

  • 而後關閉彈層後,body還能夠繼續滾動,絲絕不受影響、

  • 兼容性雖然都寫了,可是我也沒測試~

這個神不知鬼不覺的人工介入方案也是各位前輩寫爛的一個點。非常巧妙,非常經典。

 

代碼:

一、事先準備一個工具:

function getScrollOffset() {
  /*
    * @Author: @Guojufeng 
    * @Date: 2019-01-31 10:58:54 
    * 獲取頁面滾動條的距離-兼容寫法封裝
    */
  if (window.pageXOffset) {
    return {
      x: window.pageXOffset,
      y: window.pageYOffset
    }
  } else {
    return {
      x: document.body.scrollLeft || document.documentElement.scrollLeft,
      y: document.body.scrollTop || document.documentElement.scrollTop
    }
  }
};

 

二、獲取頁面的滾動距離:

/* 動態獲取當前頁面的滾動位置 */
var scrollT = null;
var LastScrollT = 0;
window.onscroll = function (e) {
  scrollT = getScrollOffset().y;//滾動條距離
}

 

三、彈層出現/消失的主流程

btn.onclick = function () {
  layer.style.display = 'block';
  // 在這裏獲取滾動的距離,賦值給body,好讓他不要跳上去。
  document.body.style.overflow = 'hidden';
  document.body.style.position = 'fixed';
  document.body.style.top = -scrollT + 'px';//改變css中top的值,配合fixed使用
  // 而後找個變量存一下剛纔的scrolltop,要否則一會從新賦值,真正的scrollT會變0
  LastScrollT = scrollT;
}
var closeBtn = document.getElementById('close');
closeBtn.onclick = function () {
  console.log(LastScrollT)
  layer.style.display = 'none';
  document.body.style.overflow = 'auto';
  document.body.style.position = 'static';

  // 關閉close彈層的時候,改變js中的scrollTop值爲上次保存的LastScrollT的值。並根據兼容性賦給對應的值。
  if (window.pageXOffset) {
    window.pageYOffset = LastScrollT;
  }else{
    document.body.scrollTop = LastScrollT;
    document.documentElement.scrollTop = LastScrollT;
  }
}

 

 

侷限問題:

這個方法我在真機上測試時發現一個問題,是IOS的:

你們應該都知道IOS的頁面頂部繼續下拉或者底部繼續上拉,都會出現頁面後邊的背景,這個在手機上很常見。可是到了這個解決方法裏邊,若是用戶在彈窗黑屏上繼續下拉漏出了底部背景,那彈層的滾動效果就都沒了。

 

我。。。

只有在這個時候,會很討厭IOS。

 

 

 

最後總結:

接着最後一個方案的問題,我返回去測試了全部方案在真機上打開彈窗時的上滑或下拉問題。

 

結論是:以上解決方案中,第四種沒有出現這種問題,第五種也沒有,共同點都是由於用了touchmove的preventDefault。

 

第二種方法和第六種有一致的狀況,若是不當心碰到了彈窗黑色蒙層的上拉下滑,而後滑的太狠出現了body的底部背景,彈層的滾動效果也就下崗了~

 

固然,這個問題也是咱們爲了測試而特地在黑色蒙層中使勁上拉下滑,倒也不至因而必現的影響用戶主要流程的問題,不知道你家的產品介不介意~

 

綜上所述,我粉第四種方案。

 

 

往後我發現更好的方法會繼續補充,也歡迎各位看官提出問題,幫我補充不足的地方。

這些方案我只是通過本身的iphone自測(沒有看安卓內的效果),哪位在項目中用了之後,測試測出什麼坑點,也懇請能告知。讓咱們一塊兒填坑,讓世界更"太平"。

 

 

 

源碼能夠到下邊的地址自取,太多文件就不貼了。

連接:https://github.com/xingorg1/jsStudy/tree/master/移動端滾動穿透

 

能夠關注個人微信公衆號看更多總結文章~

相關文章
相關標籤/搜索