iOS 的滚动穿透确实是个让人头秃的老大难问题,尤其是它的弹性滚动机制,导致简单的 CSS 锁定经常失效。你试过的 body 加 overflow: hidden 在 iOS 上不管用,是因为 Webkit 内核在处理滚动容器时,如果 body 被锁死,触摸事件依然会向上冒泡或者触发浏览器的默认回弹效果,甚至导致内部容器完全无法响应滚动手势。
这个问题的关键是:不能单纯依赖 CSS 隐藏滚动条,必须用 JS 动态改变 body 的定位状态,彻底切断页面的滚动能力,同时还要保证视觉位置不发生跳变。
目前最靠谱、兼容性最好的方案是“定位冻结法”。原理很简单:当 Popup 打开时,我们把 body 的 position 强制设为 fixed,同时把 top 设置为当前页面滚动距离的负值。这样页面虽然在视觉上还在原位,但实际上已经脱离文档流被“钉”住了,自然无法滚动。当 Popup 关闭时,再把 body 恢复原状,并瞬间把滚动条还原到之前的位置。
这个问题的关键是:不能单纯依赖 CSS 隐藏滚动条,必须用 JS 动态改变 body 的定位状态,彻底切断页面的滚动能力,同时还要保证视觉位置不发生跳变。
目前最靠谱、兼容性最好的方案是“定位冻结法”。原理很简单:当 Popup 打开时,我们把 body 的 position 强制设为 fixed,同时把 top 设置为当前页面滚动距离的负值。这样页面虽然在视觉上还在原位,但实际上已经脱离文档流被“钉”住了,自然无法滚动。当 Popup 关闭时,再把 body 恢复原状,并瞬间把滚动条还原到之前的位置。
下面给一段通用的处理逻辑,你可以直接套用到 TDesign Mobile 的 Popup 组件事件里,这里用 Vue 举例,React 或原生 JS 逻辑是一样的:
代码里的细节得注意一下。在 lockScroll 中,
top: -${scrollPosition}px这一步是核心,它利用负 margin 的原理,把 fixed 定位的 body 顶到了视觉上和原来一模一样的地方,这样用户感觉不到页面发生了重绘。在 unlockScroll 中,
window.scrollTo(0, scrollPosition)必须在样式清除后执行。如果你先滚动再清除样式,页面会先跳回顶部,然后再闪烁一下回到原位,体验极差。这种方案完全不会影响 Popup 内部的滚动。只要你的 Popup 内容容器本身设置了
overflow-y: auto且高度明确,iOS 就会独立处理这个区域的滚动,跟外层的 body 互不干扰。之前我也试过监听 touchmove 然后 preventDefault 的方案,但那个方案太容易误伤内部列表的滚动了,还是固定 Body 最省心。