前端解决滚动穿透问题

977 阅读4分钟

滚动穿透是指在移动端(或具有滚动条的容器中),当一个可滚动的模态框(或类似元素)被打开时,如果用户在该模态框内滚动到尽头,会导致底层页面(或父容器)也跟着滚动。这是一种糟糕的用户体验。

以下是前端解决滚动穿透问题的几种常见方法,以及它们的优缺点和适用场景:

1. overflow: hidden; (最简单,但有局限性)

  • 原理: 当模态框打开时,给 body 或根元素添加 overflow: hidden; 样式,阻止其滚动。模态框关闭时,移除该样式。

  • 实现 (以 Vue 为例):

    <template>
      <div>
        <button @click="showModal = true">Open Modal</button>
        <div v-if="showModal" class="modal">
          <div class="modal-content">
            <!-- 模态框内容 -->
            <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() { // 重要: 组件销毁时也要移除
        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; /* 允许模态框自身滚动 */
    }
    .modal-content{
       /* 模态框内容样式 */
    }
    </style>
    
  • 优点: 简单易用,代码量少。

  • 缺点:

    • 页面跳动:body 的滚动条被隐藏时,页面可能会因为滚动条消失而产生轻微的跳动(尤其是在 Windows 上)。
    • 丢失滚动位置: 关闭模态框后,body 的滚动位置会丢失,回到顶部。
    • 影响其他 fixed 元素: 如果页面上有其他 position: fixed 的元素(例如固定头部、固定侧边栏),它们的位置可能会受到影响,因为 fixed 定位是相对于视口的,而 bodyoverflow: hidden 可能会改变视口的计算方式。
    • 键盘可访问性问题: 如果模态框是使用键盘导航打开的,隐藏body的滚动可能会导致焦点丢失或行为异常.
  • 适用场景: 简单的模态框,不需要保留底层页面滚动位置,且页面上没有其他复杂的 fixed 元素。

2. 阻止事件冒泡 (适用于特定场景)

  • 原理: 在模态框的滚动容器上阻止 touchmove 事件的冒泡(以及可能的 wheel 事件)。

  • 实现:

    <template>
      <div v-if="showModal" class="modal">
        <div class="modal-content" @touchmove.prevent.stop @wheel.prevent.stop>
          </div>
      </div>
    </template>
    

    或使用原生JS

     modalContent.addEventListener('touchmove', function(event) {
        event.preventDefault();
        event.stopPropagation(); //有时候不需要.stop,只阻止默认行为
     }, { passive: false }); //passive:false 是关键
    
* **注意**: 必须要加上 `{ passive: false }` , 默认 passive  true,表示不会调用 preventDefault(). 如果你设置了 passive: true, 又调用了 preventDefault(), 浏览器会忽略 preventDefault(), 并报一个警告。
  • 优点: 只阻止模态框内的滚动事件,不影响其他元素。
  • 缺点:
    • 只能阻止 touchmovewheel: 无法阻止通过键盘(如 Page Up/Down)或滚动条拖动引起的滚动。
    • 模态框内部滚动问题: 如果模态框内部本身有多个可滚动区域,阻止 touchmove 会导致这些区域也无法滚动。 需要更精细的控制,例如判断当前滚动元素是否已经到达边界。
  • 适用场景: 模态框内部只有一个滚动区域,并且不需要键盘或滚动条进行滚动的情况。

3. position: fixed; (较好的解决方案)

  • 原理: 当模态框打开时,将 body 设置为 position: fixed;,并记录当前的滚动位置(scrollTop)。关闭模态框时,恢复 body 的定位,并设置回之前记录的滚动位置。

  • 实现:

    <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() { // 重要: 组件销毁时也要移除
         document.body.style.position = '';
            document.body.style.top = '';
            document.body.style.width = '';
            window.scrollTo(0, this.scrollTop);
       }
    };
    </script>
    
  • 优点:

    • 保留滚动位置: 关闭模态框后,body 的滚动位置可以恢复。
    • 避免页面跳动: position: fixed; 不会隐藏滚动条,避免了跳动问题。
    • 相对较好地兼容 fixed 元素: 虽然fixed会脱离文档流,但是因为设置了top属性为负的滚动高度,整体页面视觉上不会移动.
  • 缺点:

    • 代码略复杂: 需要记录和恢复滚动位置。
    • 兼容性问题: 在一些旧版本的 iOS Safari 中,position: fixed; 可能会导致一些布局问题(虽然现代浏览器基本已修复)。
    • 需要处理滚动条: 需要处理可能存在的滚动条,例如设置bodywidth:100%, 以防止出现水平滚动条。
  • 适用场景: 大多数情况下的模态框,需要保留底层页面滚动位置,且对兼容性要求不高。

4. 使用第三方库

  • 原理: 一些第三方库(如 body-scroll-lock)封装了更完善的滚动锁定逻辑,处理了各种边界情况和兼容性问题。

  • 实现 (以 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>
    
  • 优点:

    • 完善的解决方案: 处理了各种细节和兼容性问题。
    • 易于使用: API 简单明了。
  • 缺点: 需要引入额外的库,增加项目体积。

  • 适用场景: 对滚动锁定有较高要求,需要处理各种复杂情况,且不介意引入第三方库的项目。

5. 使用overscroll-behavior (CSS 属性,较新)

* **原理**: `overscroll-behavior` CSS 属性控制当滚动到达元素边界时发生的情况。  它可以防止滚动链(即滚动穿透)。
* **实现**:
    ```css
    .modal-content {
       overscroll-behavior: contain; /* 或 overscroll-behavior-y: contain; */
       /* 其他样式 */
    }
    ```

* **优点**:
    * **原生 CSS 解决方案**: 无需 JavaScript。
    * **性能好**: 浏览器原生支持,性能通常优于 JavaScript 解决方案。
* **缺点**:
    * **兼容性**: 比较新的属性,一些旧浏览器可能不支持(需要检查 Can I Use:[https://caniuse.com/?search=overscroll-behavior](https://caniuse.com/?search=overscroll-behavior))。 可以考虑回退方案。
* **适用场景**:  如果项目对浏览器兼容性要求不高,`overscroll-behavior` 是一个非常优雅的解决方案。

总结和选择建议:

  • 简单场景,不需保留滚动位置: overflow: hidden;
  • 模态框内只有一个滚动区域,且不需要键盘/滚动条滚动: 阻止事件冒泡
  • 大多数情况,需要保留滚动位置: position: fixed;
  • 需要处理复杂情况,不介意引入库: body-scroll-lock
  • 浏览器兼容性允许: overscroll-behavior (首选,最优雅)

最佳实践是根据项目的具体需求和目标浏览器来选择最合适的方法。通常,position: fixed;body-scroll-lock 是比较稳妥的选择。如果对兼容性要求不高,强烈推荐 overscroll-behavior。 记得在开发过程中进行充分的测试,确保在各种设备和浏览器上都能正常工作。