前端滚动穿透笔记

前端滚动穿透(Scroll Penetration)问题是指当页面上弹出一个模态框(Modal)、侧边抽屉(Drawer)或任何覆盖层时,用户在覆盖层上滑动鼠标滚轮或触摸板时,底层的页面内容也跟着一起滚动。这会给用户带来困惑,并破坏用户体验。

滚动穿透问题产生的原因

通常,滚动穿透发生在以下情况:

  1. 事件冒泡 : 覆盖层内的滚动事件没有被完全阻止,或者事件冒泡到了 bodyhtml 元素,导致底层页面接收到滚动指令。
  2. overflow 属性未正确设置 : 当模态框弹出时,底层页面的 bodyhtml 元素的 overflow 属性没有被设置为 hidden 或其他能阻止滚动的值。
  3. 移动端特殊行为 : 尤其在iOS设备上,position: fixed 元素内的滚动和底层页面的滚动行为比较特殊,即使设置了 overflow: hidden,也可能出现弹性滚动或穿透。

解决滚动穿透问题的思路和方法

解决滚动穿透问题有多种方法,可以根据具体场景和需求选择。

1. 设置 bodyhtmloverflow: hidden (最常用且简单)

这是最直接也最常用的方法。当模态框显示时,将 bodyhtml 元素的 overflow 属性设置为 hidden,阻止其滚动。当模态框关闭时,再恢复 overflow 属性。

优点:

  • 实现简单,代码量少。
  • 兼容性较好。

缺点:

  • 滚动位置丢失 : 如果页面在弹出模态框前已经滚动到某个位置,设置 overflow: hidden 后,页面会瞬间跳到顶部(滚动位置丢失)。
  • 滚动条消失/出现导致页面抖动 : 当 overflow: hidden 导致滚动条消失时,页面的宽度会增加(因为没有了滚动条占据空间),这可能导致页面内容向右移动,产生抖动。反之,滚动条出现时也会抖动。

解决方案改进 (解决滚动位置丢失和抖动) :

  • 保存滚动位置并用 position: fixed 模拟:

    1. 当模态框打开时,记录当前的 scrollTop
    2. bodyposition 设置为 fixedtop 设置为负的 scrollTop 值,width 设置为 100%
    3. 同时,为了防止滚动条消失导致的抖动,可以计算滚动条的宽度,并将其作为 padding-right 添加到 body 上。
    4. 当模态框关闭时,恢复 bodypositiontop,并移除 padding-right,然后将 scrollTop 恢复到之前保存的值。
    ini 复制代码
    let scrollTop;
    function disableBodyScroll() {
        scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        document.body.style.cssText = `
            position: fixed;
            top: -${scrollTop}px;
            left: 0;
            width: 100%;
            overflow: hidden;
            padding-right: ${window.innerWidth - document.documentElement.clientWidth}px; /* 补偿滚动条宽度 */
        `;
    }
    
    function enableBodyScroll() {
        document.body.style.cssText = '';
        document.documentElement.scrollTop = scrollTop; // 恢复滚动位置
        document.body.scrollTop = scrollTop; // 兼容旧浏览器
    }
    
    // 示例使用
    // 当模态框打开时调用 disableBodyScroll()
    // 当模态框关闭时调用 enableBodyScroll()

    注意 : window.innerWidth - document.documentElement.clientWidth 可以用来获取滚动条的宽度。

2. 阻止事件冒泡和默认行为 (适用于移动端,尤其是iOS)

在移动设备上,特别是iOS,即使设置 overflow: hidden,也可能因为弹性滚动等特性导致穿透。此时,需要更精细地控制触摸事件。

  • 在覆盖层上阻止 touchmove 默认行为 :

    在模态框的滚动容器上,监听 touchmove 事件,并阻止其默认行为。

    javascript 复制代码
    const modalContent = document.querySelector('.modal-content'); // 模态框内部可滚动区域
    if (modalContent) {
        modalContent.addEventListener('touchmove', (e) => {
            // 检查是否滚动到了顶部或底部
            const isAtTop = modalContent.scrollTop === 0;
            const isAtBottom = modalContent.scrollHeight - modalContent.scrollTop === modalContent.clientHeight;
    
            // 如果已经滚动到顶部且继续向上滑动,或者滚动到底部且继续向下滑动,则阻止默认行为
            // 否则,允许模态框内部滚动
            if ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0)) {
                e.preventDefault();
            }
            e.stopPropagation(); // 阻止事件冒泡到body
        }, { passive: false }); // 设置 passive: false 以允许阻止默认行为
    }
    
    // 在模态框外部的遮罩层上,直接阻止所有 touchmove
    const modalOverlay = document.querySelector('.modal-overlay');
    if (modalOverlay) {
        modalOverlay.addEventListener('touchmove', (e) => {
            e.preventDefault();
        }, { passive: false });
    }

    注意:

    • passive: false 是关键,它告诉浏览器你可能会调用 preventDefault(),否则 preventDefault() 可能无效。
    • 对于模态框内部有滚动区域的情况,需要判断是否滚动到了顶部或底部,只在此时阻止默认行为,否则会阻止模态框内部的正常滚动。
  • touch-action: none (CSS 属性) :
    touch-action CSS 属性可以用来指定触摸屏用户如何与元素进行交互。设置为 none 可以阻止所有的平移和捏合手势。

    sql 复制代码
    .modal-overlay {
        touch-action: none; /* 阻止所有触摸手势 */
    }

    优点 : 纯CSS实现,简单。
    缺点:

    • 兼容性不如JS事件监听广泛(旧浏览器可能不支持)。
    • 只阻止了元素本身的触摸行为,如果事件冒泡到父元素,可能仍然会穿透。
    • 通常需要与JS方法结合使用,或作为辅助手段。

3. 禁用底层页面滚动条 (不推荐,但作为思路了解)

在模态框显示时,将底层页面的滚动条隐藏,并让模态框自身拥有滚动条。这种方法比较复杂,且容易出现问题,一般不推荐。

4. 使用成熟的UI库或框架

如果你正在使用React、Vue、Angular等框架,并使用了它们提供的UI组件库(如Ant Design, Element UI, Material UI等),那么它们的模态框组件通常已经内置了滚动穿透的解决方案,你无需手动处理。

例如,Ant Design 的 Modal 组件在打开时会自动在 body 上添加 overflow: hiddenpadding-right 来处理。

5. 针对iOS的特殊处理

iOS的WebView在处理 position: fixed 和滚动时有其独特之处。

  • position: fixed 在iOS上的问题 : 在某些iOS版本中,当软键盘弹出时,position: fixed 的元素可能会失效或错位。这与滚动穿透问题略有不同,但都与布局和滚动有关。
  • -webkit-overflow-scrolling: touch: 这个CSS属性可以改善iOS上滚动体验,使其更流畅,但与滚动穿透问题关系不大。

总结与选择

  • 最推荐的方案 : 方案1的改进版 (保存滚动位置,使用 position: fixedpadding-right 补偿滚动条)。它兼顾了用户体验(不丢失滚动位置,不抖动)和实现复杂度。
  • 移动端(尤其是iOS)补充方案 : 结合方案2 ,在模态框的遮罩层和内容区域(如果可滚动)上监听 touchmove 事件并阻止默认行为,以应对弹性滚动和事件冒泡。
  • 使用UI库: 如果项目允许,直接使用成熟的UI库提供的模态框组件是最省心、最可靠的方式。

在实际开发中,你可能需要结合多种方法,并在不同设备上进行充分测试,以确保最佳的兼容性和用户体验。

相关推荐
yuren_xia5 分钟前
Spring Boot中保存前端上传的图片
前端·spring boot·后端
普通网友1 小时前
Web前端常用面试题,九年程序人生 工作总结,Web开发必看
前端·程序人生·职场和发展
站在风口的猪11083 小时前
《前端面试题:CSS对浏览器兼容性》
前端·css·html·css3·html5
青莳吖4 小时前
使用 SseEmitter 实现 Spring Boot 后端的流式传输和前端的数据接收
前端·spring boot·后端
CodeCraft Studio5 小时前
PDF处理控件Aspose.PDF教程:在 C# 中更改 PDF 页面大小
前端·pdf·c#
拉不动的猪5 小时前
TS常规面试题1
前端·javascript·面试
再学一点就睡5 小时前
实用为王!前端日常工具清单(调试 / 开发 / 协作工具全梳理)
前端·资讯·如何当个好爸爸
Jadon_z6 小时前
vue2 项目中 npm run dev 运行98% after emitting CopyPlugin 卡死
前端·npm
一心赚狗粮的宇叔6 小时前
web全栈开发学习-01html基础
前端·javascript·学习·html·web
IT瘾君6 小时前
JavaWeb:前端工程化-ElementPlus
前端·elementui·node.js·vue