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


 

在移動端中,如果我們使用了一個固定定位的遮罩層,且其下方的dom結構的寬度|高度超出屏幕的寬度|高度,那么即使遮罩層彈出后鋪滿了整個屏幕,其下方的dom結構依然可以滾動,這就是大家所說的“滾動穿透”。

而且經常是你在pc模擬器上沒有問題,但是真機打開就一定會出現。

 

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

 

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

 

各方法操作難易不同,分別針對彈層和body是否超出一屏可滾動等不同情況。看官可以對症下葯。

 

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

 

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

 

適用場景需滿足以下條件:

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

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

    3、彈層不用滾動效果

 

解決方案:

彈層出現時,用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不能滾動,那么在項目中用這個方法,還是性價比很高的。

 

 

 

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

 

適用場景需滿足以下條件:

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

    2、雖然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);

 

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

 

適用場景:

  1、(適用)body可滾動

  2、(適用)觸發彈層出現的按鈕可以在任意位置



需滿足以下條件:

    1、(需滿足)彈層內容不需要滾動

 

解決方案:

當彈層出現的時候不需要再禁掉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的默認事件,以阻止滾動穿透。

 

同樣,如果彈層中需要滾動效果,則不能解決了。那么這時,就引來我們的主題難點,可以有以下幾種思路解決:

 

 

 

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

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

 

適用以下場景:

    1、body可滾動

    2、觸發彈層出現的按鈕可以在任意位置

    3、彈層可以滾動

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

 

解決方案:

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

 

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

 

所以依舊需要同樣的代碼,對可滾動區域的touchmove做監聽:若到頂或到底,同樣阻止默認事件。

 

需要做的事情有:

1、預存一個全局變量targetY

 

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

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

 

3、后期touchmove里邊獲取每次的e.targetTouches[0].clientY與第一次的進行比較,可以得出用戶是上滑還是下滑手勢。

 

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

同理,如果手勢是向下滑,並且當前滾動高度為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';
    // 彈窗關閉后,可解除所有禁止 - 懶人就不寫了
}

 

 

 

 

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

 

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

 

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

 

適用場景:

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

 

解決方案與思路:

具體制作思路寫在js注釋上。

 

1、交互代碼

 

/* 交互代碼 */
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';
  // 彈窗關閉后,可解除所有禁止 - 懶人就不寫了
}

 

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

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

 

 

3、重寫手勢滑動效果

/* 重新寫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里邊:
1、在touchstart的時候,監聽用戶手勢按下,記錄初次按下的坐標點y的值y1。

 

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

 

3、然后通過計算y1和y2 的差值判斷出用戶是朝哪個方向移動的手勢。

 

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

 

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

 

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

 

7、另外這里還可以在touchend事件里,把touchstart和touchmove包括自身touchend的事件都解綁掉。我偷懶就不寫了。

 

問題局限:

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

 

 

 

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

 

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

 

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

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

 

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

 

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

  • body可以繼續滾動、彈層出來后他的top值限制他不會跳到頂部、

  • 彈層中不管短還是長,需不需要滾動,都不care,自由活動、

  • 然后關閉彈層后,body還可以繼續滾動,絲毫不受影響、

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

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

 

代碼:

1、事先准備一個工具:

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

 

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

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

 

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

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/移動端滾動穿透

 

可以關注我的微信公眾號看更多總結文章~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM