返回博客

微信小程序弹窗滚动穿透:scroll-view 是杀手,4 件套才能封死

给一个小程序自定义弹窗加锁滚,直觉上就是在 mask 上加一个 catch:touchmove="noop"——写完测一下,mask 半透明区域确实拦住了。以为大功告成。第二天同事测反馈:弹窗内列表滑到底,背景跟着滚;偶尔还有关闭弹窗后页面"跳位置"的问题。

这不是单点缺失,是结构性 bug。必须 4 件套协同,缺一件就留一个穿透入口。

现象

自定义弹窗(非 van-popup)打开期间:

  1. 手指在弹窗内列表滑到边界后,背景页面跟着滚
  2. 手指在 mask 半透明区域滑动,背景仍可响应(某些机型)
  3. 关闭弹窗后,页面滚动位置发生漂移

根因

反模式:弹窗内用 scroll-view

<view class="mask" catch:touchmove="noop">
  <view class="popup">
    <scroll-view scroll-y style="max-height: 60vh;">
      <view wx:for="{{items}}">...</view>
    </scroll-view>
  </view>
</view>

看起来没问题——mask 拦了 touchmove,scroll-view 让内容可滚。实际上这是双重炸弹:

炸弹一<scroll-view> 是微信 native 组件,touch 事件不参与 WebView 冒泡链。mask 上的 catch:touchmove 永远收不到 scroll-view 内部的 touch,等于对 scroll-view 区域完全失效。

炸弹二:scroll chaining。scroll-view 滚到顶/底边界后,剩余的滑动 delta 会释放给 page-scroll——这是 iOS/Android 原生 scrollable 控件的设计行为,小程序层面无法靠 catch 阻止。

坑:scroll-view 不是无副作用的 view 升级版

scroll-viewview 不在同层抽象。view 渲染在 WebView 内,touch 事件正常冒泡;scroll-view 是 native 组件,touch 不冒泡、滚到边界会触发 scroll chaining。把"弹窗内需要滚动"机械等价于"必须用 scroll-view"是典型的错误代换——改组件类型前必须想清楚事件链和滚动链的影响。

正解:4 件套,缺一不可

件 1:page-meta 锁 page

<page-meta page-style="{{ visible ? 'overflow: hidden' : '' }}" />

兜底锁住 page-scroll 本身,防止极少数 iOS 机型直接响应 native 滚动条。必须动态绑定——弹窗关闭时清空,否则页面其他正常滚动会被冻结。

件 2:mask 上 catch:touchmove

<view wx:if="{{visible}}" class="mask" catch:touchmove="noop" bindtap="close">
noop() {},  // 占位空函数,必须用 catch: 不能用 bind:

拦截 mask 半透明区域的 touchmove,防止冒泡触发 page-scroll。

件 3:popup 容器用 CSS overflow-y: auto

<view class="popup">
  <view class="popup-header">标题</view>
  <view class="popup-body">
    <!-- 内容列表 -->
  </view>
</view>
.popup-body {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch; /* iOS 惯性滚动 */
}

CSS overflow 是 WebView 层行为,touch 事件正常冒泡——mask 的 catch:touchmove 能完整覆盖整个弹窗区域,不存在 native 组件的事件黑洞问题。

件 4:内层列表用 view,禁用 scroll-view

<view class="popup-body">
  <view class="item" wx:for="{{items}}" wx:key="id">{{item.text}}</view>
</view>

保持事件链完整。配合件 3 的 overflow,内容超出时由 popup 容器自身滚动,touch 事件正常冒泡到 mask。

缺一件对照表

| 缺哪件 | 留下哪个穿透入口 | |--------|-----------------| | 件 1(page-meta)| 特定 iOS 机型 native 滚动条仍响应 | | 件 2(mask catch)| 用户在 mask 半透明边缘滑动时背景滚 | | 件 3(popup overflow)| 弹窗内容超出可视区无法查看 | | 件 4(用了 scroll-view)| 弹窗内滑到边界后背景跟着滚(scroll chaining) |

正解:CSS overflow 替代 scroll-view,4 件套协同

弹窗容器自身设 overflow-y: auto,内层列表用普通 view——不需要 scroll-view,touch 事件链完整,mask 的 catch:touchmove 能完整覆盖。再配合 page-meta 动态锁 page overflow,4 件套封死所有穿透路径。

适用范围说明

本方案仅适用于纯自定义弹窗(自己写 mask + popup 容器)。如果用的是 vant-weapp 的 <van-popup>,不要叠加本方案的 catch:touchmove——van-popup 的 overlay 已自带拦截,再叠加会触发 iOS WebKit 的 scroll-session-cancellation,反而让弹窗内的 scroll-view 失效。van-popup 场景应改用 page-meta 3 件套方案。

一句话外卖

微信小程序里"让弹窗内可滚动"不等于"用 scroll-view"——scroll-view 是 native 组件,touch 不冒泡、滚到边界会 chain 到背景;改用 CSS overflow-y: auto 配合 page-meta 锁 page,才是真正封死穿透的结构性解法。