返回博客

van-popup 锁滚三件套:lock-scroll 只锁遮罩,背景穿透靠 page-meta

一个 C 端小程序的列表页,用 <van-popup> 做了一个筛选弹窗。模拟器里一切正常,真机测试时用户反馈:弹窗打开期间背景列表还能被拖动。加了 lock-scroll 也没用,甚至把弹窗内容换成一句话(内容很短)后穿透更明显。折腾了好几轮,最后发现根因完全不在弹窗本身。

现象

  1. 背景穿透:popup 打开后,手指在遮罩区或 popup 内向上滑,背景列表跟着滚
  2. 短内容必现:弹窗内容不足 max-height 时,穿透 100% 复现
  3. 长内容真机不可滚:换成长列表后,popup 内容在模拟器(鼠标滚轮)能滚,真机(手指触摸)却完全无响应
  4. lock-scroll 默认已开,设不设没有区别

根因

翻 vant-weapp 源码,van-popuplock-scroll 实现只有一行:给内部的 <van-overlay> 节点注册 catch:touchmove="noop"

这意味着它只能拦截用户手指直接落在半透明遮罩区的滑动,不对 page 本身做任何处理。

当弹窗内容较短、用户手指落在 popup 白色区域内上滑,touch 事件冒泡路径是:popup 内容 → van-popup 容器 → page——整条路上没有任何 catch 拦截,page 照常响应滚动。

坑:van-popup lock-scroll 仅锁遮罩,page 本身没有被锁

lock-scroll 的作用域只有 van-overlay 这一个元素。只要触摸起点不落在 overlay 上(比如落在 popup 白色面板内),touch 事件就会正常冒泡到 page,背景依然可以滚动。这是 vant-weapp 的架构设计,使用方必须自行补 page-meta

长内容真机不可滚的问题则是另一个坑的叠加:有人试图用 <view style="overflow-y: auto"> 替代 scroll-view 做内容滚动,iOS WebKit 在嵌套触摸链路下对 CSS overflow 的 scroll gesture recognizer 启动极为苛刻,几乎必然失效。

正解

三件套,每件负责独立的一层,互不干扰:

件 1:van-popup——负责遮罩 + 点击关闭,不做任何额外改动

<van-popup
  show="{{popupVisible}}"
  round
  close-on-click-overlay
  bind:close="closePopup"
  custom-style="width: 86%; max-width: 700rpx; max-height: 80vh;"
>

件 2:scroll-view——弹窗内可滚动内容必须用原生 scroll-view,绝不用 <view style="overflow-y:auto">,也绝不在 scroll-view 或其任何祖先上加 catch:touchmove

<view class="popup-container">
  <view class="popup-header" style="flex-shrink:0;">标题</view>
  <scroll-view scroll-y class="popup-scroll-body">
    <view wx:for="{{items}}" wx:key="id">{{item.text}}</view>
  </scroll-view>
</view>
.popup-container {
  display: flex;
  flex-direction: column;
  max-height: 80vh;
  padding: 32rpx;
  box-sizing: border-box;
}
.popup-scroll-body {
  flex: 1;       /* 占剩余空间 */
  min-height: 0; /* 消除 flex 内 min-height:auto 撑开问题 */
  overflow: hidden;
}

件 3:page-meta——CSS 级别锁 page,动态绑定 popup 显示状态,完全不碰事件链

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

同一页面有多个 popup 时,用 || 聚合所有 show 状态:

<page-meta page-style="{{ showFilter || showFaq || showDate ? 'overflow: hidden;' : '' }}" />
正解:page-meta CSS 锁 page,scroll-view 锁内容,各司其职

三层分工明确:van-overlay 拦截遮罩区触摸 / scroll-view 原生消化弹窗内滑动 / page-meta overflow:hidden 兜底锁死 page 本身。每层独立,不存在事件链交叉,iOS 真机完全兼容。

反模式:给 van-popup 外层套自定义 mask + catch:touchmove
<!-- 错误写法 -->
<view class="my-mask" catch:touchmove="noop">
  <van-popup show="{{visible}}">
    <view style="overflow-y: auto">...</view>
  </van-popup>
</view>

父层的 catch:touchmove 会触发 iOS WebKit 的 scroll-session-cancellation:整条触摸链路被父节点截断,子 scroll-view 的 native scroll 手势因此无法启动,弹窗内容真机不可滚。

排查清单

遇到 van-popup 背景穿透或内容不可滚,按顺序检查:

  1. 全文 grep page-meta,确认存在且 page-style 动态绑定了 popup 状态
  2. grep catch:touchmove,确认 van-popup 父链上没有任何 catch
  3. 弹窗内容容器用的是 <scroll-view> 而非 <view style="overflow-y:auto">
  4. scroll-view 高度链完整:容器有 flex:1; min-height:0
  5. 多弹窗场景:page-meta 条件是否 OR 覆盖了所有 popup 的 show 状态

一句话外卖

van-popup 的 lock-scroll 只是遮罩的局部补丁,锁住整页滚动必须靠 page-meta overflow:hidden;弹窗内可滚动区域必须用 <scroll-view>,任何 catch:touchmove 在其祖先上出现都会让 iOS 真机滚动失效。