前端如何解決滾動(dòng)穿透問(wèn)題詳解
前言
滾動(dòng)穿透是指在移動(dòng)端(或具有滾動(dòng)條的容器中),當(dāng)一個(gè)可滾動(dòng)的模態(tài)框(或類似元素)被打開(kāi)時(shí),如果用戶在該模態(tài)框內(nèi)滾動(dòng)到盡頭,會(huì)導(dǎo)致底層頁(yè)面(或父容器)也跟著滾動(dòng)。這是一種糟糕的用戶體驗(yàn)。
以下是前端解決滾動(dòng)穿透問(wèn)題的幾種常見(jiàn)方法,以及它們的優(yōu)缺點(diǎn)和適用場(chǎng)景:
1. overflow: hidden; (最簡(jiǎn)單,但有局限性)
原理: 當(dāng)模態(tài)框打開(kāi)時(shí),給
body
或根元素添加overflow: hidden;
樣式,阻止其滾動(dòng)。模態(tài)框關(guān)閉時(shí),移除該樣式。實(shí)現(xiàn) (以 Vue 為例):
<template> <div> <button @click="showModal = true">Open Modal</button> <div v-if="showModal" class="modal"> <div class="modal-content"> <!-- 模態(tài)框內(nèi)容 --> <button @click="showModal = false">Close</button> </div> </div> </div> </template> <script> export default { data() { return { showModal: false, }; }, watch: { showModal(newValue) { if (newValue) { document.body.style.overflow = 'hidden'; } else { document.body.style.overflow = ''; // 或 'auto' } }, }, beforeDestroy() { // 重要: 組件銷毀時(shí)也要移除 document.body.style.overflow = ''; } }; </script> <style> .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); overflow-y: auto; /* 允許模態(tài)框自身滾動(dòng) */ } .modal-content{ /* 模態(tài)框內(nèi)容樣式 */ } </style>
優(yōu)點(diǎn): 簡(jiǎn)單易用,代碼量少。
缺點(diǎn):
- 頁(yè)面跳動(dòng): 當(dāng)
body
的滾動(dòng)條被隱藏時(shí),頁(yè)面可能會(huì)因?yàn)闈L動(dòng)條消失而產(chǎn)生輕微的跳動(dòng)(尤其是在 Windows 上)。 - 丟失滾動(dòng)位置: 關(guān)閉模態(tài)框后,
body
的滾動(dòng)位置會(huì)丟失,回到頂部。 - 影響其他 fixed 元素: 如果頁(yè)面上有其他
position: fixed
的元素(例如固定頭部、固定側(cè)邊欄),它們的位置可能會(huì)受到影響,因?yàn)?nbsp;fixed
定位是相對(duì)于視口的,而body
的overflow: hidden
可能會(huì)改變視口的計(jì)算方式。 - 鍵盤(pán)可訪問(wèn)性問(wèn)題: 如果模態(tài)框是使用鍵盤(pán)導(dǎo)航打開(kāi)的,隱藏
body
的滾動(dòng)可能會(huì)導(dǎo)致焦點(diǎn)丟失或行為異常.
- 頁(yè)面跳動(dòng): 當(dāng)
適用場(chǎng)景: 簡(jiǎn)單的模態(tài)框,不需要保留底層頁(yè)面滾動(dòng)位置,且頁(yè)面上沒(méi)有其他復(fù)雜的 fixed 元素。
2. 阻止事件冒泡 (適用于特定場(chǎng)景)
原理: 在模態(tài)框的滾動(dòng)容器上阻止
touchmove
事件的冒泡(以及可能的wheel
事件)。實(shí)現(xiàn):
<template> <div v-if="showModal" class="modal"> <div class="modal-content" @touchmove.prevent.stop @wheel.prevent.stop> </div> </div> </template>
或使用原生JS
javascript 代碼解讀復(fù)制代碼 modalContent.addEventListener('touchmove', function(event) { event.preventDefault(); event.stopPropagation(); //有時(shí)候不需要.stop,只阻止默認(rèn)行為 }, { passive: false }); //passive:false 是關(guān)鍵
* **注意**: 必須要加上 `{ passive: false }` , 默認(rèn) passive 是 true,表示不會(huì)調(diào)用 preventDefault(). 如果你設(shè)置了 passive: true, 又調(diào)用了 preventDefault(), 瀏覽器會(huì)忽略 preventDefault(), 并報(bào)一個(gè)警告。
- 優(yōu)點(diǎn): 只阻止模態(tài)框內(nèi)的滾動(dòng)事件,不影響其他元素。
- 缺點(diǎn):
- 只能阻止 touchmove 和 wheel: 無(wú)法阻止通過(guò)鍵盤(pán)(如 Page Up/Down)或滾動(dòng)條拖動(dòng)引起的滾動(dòng)。
- 模態(tài)框內(nèi)部滾動(dòng)問(wèn)題: 如果模態(tài)框內(nèi)部本身有多個(gè)可滾動(dòng)區(qū)域,阻止
touchmove
會(huì)導(dǎo)致這些區(qū)域也無(wú)法滾動(dòng)。 需要更精細(xì)的控制,例如判斷當(dāng)前滾動(dòng)元素是否已經(jīng)到達(dá)邊界。
- 適用場(chǎng)景: 模態(tài)框內(nèi)部只有一個(gè)滾動(dòng)區(qū)域,并且不需要鍵盤(pán)或滾動(dòng)條進(jìn)行滾動(dòng)的情況。
3. position: fixed; (較好的解決方案)
原理: 當(dāng)模態(tài)框打開(kāi)時(shí),將
body
設(shè)置為position: fixed;
,并記錄當(dāng)前的滾動(dòng)位置(scrollTop
)。關(guān)閉模態(tài)框時(shí),恢復(fù)body
的定位,并設(shè)置回之前記錄的滾動(dòng)位置。實(shí)現(xiàn):
<template> <div v-if="showModal" class="modal" @touchmove.prevent> </div> </template> <script> export default { data() { return { showModal: false, scrollTop: 0, }; }, watch: { showModal(newValue) { if (newValue) { this.scrollTop = window.pageYOffset || document.documentElement.scrollTop; document.body.style.position = 'fixed'; document.body.style.top = `-${this.scrollTop}px`; document.body.style.width = '100%'; // 確保 body 寬度占滿 } else { document.body.style.position = ''; document.body.style.top = ''; document.body.style.width = ''; window.scrollTo(0, this.scrollTop); } }, }, beforeDestroy() { // 重要: 組件銷毀時(shí)也要移除 document.body.style.position = ''; document.body.style.top = ''; document.body.style.width = ''; window.scrollTo(0, this.scrollTop); } }; </script>
優(yōu)點(diǎn):
- 保留滾動(dòng)位置: 關(guān)閉模態(tài)框后,
body
的滾動(dòng)位置可以恢復(fù)。 - 避免頁(yè)面跳動(dòng):
position: fixed;
不會(huì)隱藏滾動(dòng)條,避免了跳動(dòng)問(wèn)題。 - 相對(duì)較好地兼容 fixed 元素: 雖然
fixed
會(huì)脫離文檔流,但是因?yàn)樵O(shè)置了top
屬性為負(fù)的滾動(dòng)高度,整體頁(yè)面視覺(jué)上不會(huì)移動(dòng).
- 保留滾動(dòng)位置: 關(guān)閉模態(tài)框后,
缺點(diǎn):
- 代碼略復(fù)雜: 需要記錄和恢復(fù)滾動(dòng)位置。
- 兼容性問(wèn)題: 在一些舊版本的 iOS Safari 中,
position: fixed;
可能會(huì)導(dǎo)致一些布局問(wèn)題(雖然現(xiàn)代瀏覽器基本已修復(fù))。 - 需要處理滾動(dòng)條: 需要處理可能存在的滾動(dòng)條,例如設(shè)置
body
的width:100%
, 以防止出現(xiàn)水平滾動(dòng)條。
適用場(chǎng)景: 大多數(shù)情況下的模態(tài)框,需要保留底層頁(yè)面滾動(dòng)位置,且對(duì)兼容性要求不高。
4. 使用第三方庫(kù)
原理: 一些第三方庫(kù)(如
body-scroll-lock
)封裝了更完善的滾動(dòng)鎖定邏輯,處理了各種邊界情況和兼容性問(wèn)題。實(shí)現(xiàn) (以 body-scroll-lock 為例):
npm install body-scroll-lock
<template> <div v-if="showModal" ref="modal" class="modal"> <div class="modal-content"> </div> </div> </template> <script> import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock'; export default { data() { return { showModal: false, }; }, watch: { showModal(newValue) { if (newValue) { disableBodyScroll(this.$refs.modal); } else { enableBodyScroll(this.$refs.modal); } }, }, beforeDestroy() { clearAllBodyScrollLocks(); // 清除所有鎖定 }, }; </script>
優(yōu)點(diǎn):
- 完善的解決方案: 處理了各種細(xì)節(jié)和兼容性問(wèn)題。
- 易于使用: API 簡(jiǎn)單明了。
缺點(diǎn): 需要引入額外的庫(kù),增加項(xiàng)目體積。
適用場(chǎng)景: 對(duì)滾動(dòng)鎖定有較高要求,需要處理各種復(fù)雜情況,且不介意引入第三方庫(kù)的項(xiàng)目。
5. 使用overscroll-behavior (CSS 屬性,較新)
***原理**: `overscroll-behavior` CSS 屬性控制當(dāng)滾動(dòng)到達(dá)元素邊界時(shí)發(fā)生的情況。 它可以防止?jié)L動(dòng)鏈(即滾動(dòng)穿透)。 * **實(shí)現(xiàn)**: ```css .modal-content { overscroll-behavior: contain; /* 或 overscroll-behavior-y: contain; */ /* 其他樣式 */ } ``` * **優(yōu)點(diǎn)**: * **原生 CSS 解決方案**: 無(wú)需 JavaScript。 * **性能好**: 瀏覽器原生支持,性能通常優(yōu)于 JavaScript 解決方案。 * **缺點(diǎn)**: * **兼容性**: 比較新的屬性,一些舊瀏覽器可能不支持(需要檢查 Can I Use:[https://caniuse.com/?search=overscroll-behavior](https://caniuse.com/?search=overscroll-behavior))。 可以考慮回退方案。 * **適用場(chǎng)景**: 如果項(xiàng)目對(duì)瀏覽器兼容性要求不高,`overscroll-behavior` 是一個(gè)非常優(yōu)雅的解決方案。
總結(jié)和選擇建議:
- 簡(jiǎn)單場(chǎng)景,不需保留滾動(dòng)位置:
overflow: hidden;
- 模態(tài)框內(nèi)只有一個(gè)滾動(dòng)區(qū)域,且不需要鍵盤(pán)/滾動(dòng)條滾動(dòng): 阻止事件冒泡
- 大多數(shù)情況,需要保留滾動(dòng)位置:
position: fixed;
- 需要處理復(fù)雜情況,不介意引入庫(kù):
body-scroll-lock
- 瀏覽器兼容性允許:
overscroll-behavior
(首選,最優(yōu)雅)
最佳實(shí)踐是根據(jù)項(xiàng)目的具體需求和目標(biāo)瀏覽器來(lái)選擇最合適的方法。通常,position: fixed;
或 body-scroll-lock
是比較穩(wěn)妥的選擇。如果對(duì)兼容性要求不高,強(qiáng)烈推薦 overscroll-behavior
。 記得在開(kāi)發(fā)過(guò)程中進(jìn)行充分的測(cè)試,確保在各種設(shè)備和瀏覽器上都能正常工作。
總結(jié)
到此這篇關(guān)于前端如何解決滾動(dòng)穿透問(wèn)題的文章就介紹到這了,更多相關(guān)前端滾動(dòng)穿透問(wèn)題內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS 非圖片動(dòng)態(tài)loading效果實(shí)現(xiàn)代碼
功能說(shuō)明:譬如在按某個(gè)button時(shí),顯示消息"Loading”,然后每隔一秒后后面加上".",至一定數(shù)量的"."時(shí)如:"Loading...",再重置此消息為"Loading",繼續(xù)動(dòng)態(tài)顯示,直至按鈕事件處理完成。2010-04-04JavaScript實(shí)現(xiàn)簡(jiǎn)單的音樂(lè)播放器
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)簡(jiǎn)單的音樂(lè)播放器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08阻止JavaScript事件冒泡傳遞(cancelBubble 、stopPropagation)
阻止JavaScript事件冒泡傳遞(cancelBubble 、stopPropagation)...2007-05-05微信小程序接入微信支付實(shí)現(xiàn)過(guò)程詳解
這篇文章主要介紹了微信小程序接入微信支付實(shí)現(xiàn)過(guò)程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-12-12JS傳遞對(duì)象數(shù)組為參數(shù)給后端,后端獲取的實(shí)例代碼
下面小編就為大家?guī)?lái)一篇JS傳遞對(duì)象數(shù)組為參數(shù)給后端,后端獲取的實(shí)例代碼。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-06-06document.getElementById的一些細(xì)節(jié)
document.getElementById的一些細(xì)節(jié)...2006-09-09