小程序或移动端『滚动穿透』与『滚动溢出』解决方案

滚动穿透

问题描述

在移动端 WEB 开发的时候(小程序也雷同),如上录屏所示,如果页面超过一屏高度出现滚动条时,在 fixed 定位的弹窗遮罩层上进行滑动,它下面的内容也会跟着一起滚动,看起来好像事件穿透到下面的DOM元素上一样,我们姑且称之为滚动穿透。

问题原因

能够猜想是文档(document)的滚动事件被触发了,如果能禁用滚动事件就好办了。

案例伪代码

html 复制代码
<div class="btn">点击出现弹窗</div>

<div class="popup">
  <div class="popup-mask"></div>
  <div class="popup-body popup-bottom">
    <div class="header">我是标题</div>
    <div class="content">
      <div>0</div>        
      <div>1</div>
      <div>...</div>
    </div>
  </div>
</div>
css 复制代码
.popup-mask {
  background-color: rgba(0, 0, 0, 0.5);
  position: fixed;
  z-index: 998;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.popup-body {
  padding: 0 50px 40px;
  background-color: #fff;
  position: fixed;
  z-index: 999;
}

✅ 解决方案A (touch-action)

默认情况下,平移(滚动)和缩放手势由浏览器专门处理,但是可以通过 CSS 特性 touch-action 来改变触摸手势的行为。摘取几个 touch-action 的值如下。

描述
auto 启用浏览器处理所有平移和缩放手势。
none 禁用浏览器处理所有平移和缩放手势。
manipulation 启用平移和缩放手势,但禁用其他非标准手势,例如双击缩放。
pinch-zoom 启用页面的多指平移和缩放。

于是在 popup 元素上设置该属性,禁用元素(及其不可滚动的后代)上的所有手势就可以解决该问题了。

css 复制代码
.popup {
  touch-action: none;
}

Note: 无障碍设计 阻止页面缩放可能会影响视力不佳的人阅读和理解页面内容,不过小程序本身好像就不可以缩放!

✅ 解决方案B (event.preventDefault)

来自 W3C 的一个标准

大意是说,在 touchstart 和 touchmove 事件中调用 preventDefault 方法可以阻止任何关联事件的默认行为,包括鼠标事件和滚动。

因此我们可以这样处理。

Step 1、监听弹窗最外层元素(popup)的 touchmove 事件并阻止默认行为来禁用所有滚动(包括弹窗内部的滚动元素)。

Step 2、释放弹窗内的滚动元素,允许其滚动:同样监听 touchmove 事件,但是阻止该滚动元素的冒泡行为(stopPropagation),使得在滚动的时候最外层元素(popup)无法接收到 touchmove 事件。

js 复制代码
const popup = document.querySelector('.popup')
const scrollBox = document.querySelector('.content')

popup.addEventListener('touchmove', (e) => {
  // Step 1: 阻止默认事件
  e.preventDefault()
})

scrollBox.addEventListener('touchmove', (e) => {
  // Step 2: 阻止冒泡
  e.stopPropagation()
})

滚动溢出

问题描述

如上录屏所示,弹窗内也含有滚动元素,在滚动元素滚到底部或顶部时,再往下或往上滚动,也会触发页面的滚动,这种现象称之为滚动链(scroll chaining), 但是感觉滚动溢出(overscroll)这个名字更言辞达意。

❌ 解决方案A (overscroll-behavior)

overscroll-behavior 是 CSS 的一个特性,允许控制浏览器滚动到边界的表现,它有如下几个值。

描述
auto 默认效果,元素的滚动可以传播到祖先元素。
contain 阻止滚动链,滚动不会传播到祖先元素,但是会显示节点自身的局部效果。例如 Android 上过度滚动的发光效果或 iOS 上的橡皮筋效果。
none 与 contain 相同,但是会阻止自身的过度效果。

所以可以这样解决问题:

css 复制代码
.content {
  overscroll-behavior: none;
}

简洁干净高性能,不过 Safari 全系不支持,兼容性如下,有没有感觉 Safari 就是现代版的 IE(偶然听路人说的)!

✅ 解决方案B (event.preventDefault)

借用 event.preventDefault 的能力,当组件滚动到底部或顶部时,通过调用 event.preventDefault 阻止所有滚动,从而页面滚动也不会触发了,而在滚动之间则不做处理。

js 复制代码
let initialPageY = 0

scrollBox.addEventListener('touchstart', (e) => {
    initialPageY = e.changedTouches[0].pageY
})

scrollBox.addEventListener('touchmove', (e) => {
    const deltaY = e.changedTouches[0].pageY - initialPageY
    
    // 禁止向上滚动溢出
    if (e.cancelable && deltaY > 0 && scrollBox.scrollTop <= 0) {
        e.preventDefault()
    }

    // 禁止向下滚动溢出
    if (
        e.cancelable &&
        deltaY < 0 && 
        scrollBox.scrollTop + scrollBox.clientHeight >= scrollBox.scrollHeight
    ) {
        e.preventDefault()
    }
})

解决方案完整 Demo

github.com/Barrior/ca....

相关推荐
摇滚侠1 小时前
方法 A 等方法 B 执行完再执行 叫同步调用还是异步调用 JS 默认是同步调用还是异步调用
开发语言·javascript·ecmascript
触底反弹2 小时前
🔥 字符串算法面试三连击:反转、回文、回文变种,搞懂这三题稳了!
前端·javascript·算法
触底反弹2 小时前
AI Tool Use 深度解析:大模型是如何"突破物理限制"调用外部工具的?
javascript·人工智能·后端
竹林8182 小时前
从 RPC 超时到批量签名:我用 @solana/web3.js 重构了一个 NFT 铸造页面,踩了这些坑
前端·javascript
优雅格子衫3 小时前
TypeScript 类的基本使用小结
javascript·ubuntu·typescript
橘子星3 小时前
从零手写 RAG 语义检索:基于 Node.js 实现轻量级向量搜索
javascript·人工智能
林希_Rachel_傻希希3 小时前
web性能优化之————图片效果
前端·javascript·面试
橘子星3 小时前
基于 MCP 协议实现本地文件读取工具服务开发实践
javascript·人工智能
Darling噜啦啦3 小时前
前端存储与 this 指向完全指南:从 LocalStorage 实战到 call/apply/bind 深度解析
前端·javascript
sugar__salt3 小时前
手撕字符串算法:反转、回文、验证回文 Ⅱ 完整拆解
javascript·算法·面试·职场和发展