van-popup 锁滚三件套:lock-scroll 只锁遮罩,背景穿透靠 page-meta
一个 C 端小程序的列表页,用 <van-popup> 做了一个筛选弹窗。模拟器里一切正常,真机测试时用户反馈:弹窗打开期间背景列表还能被拖动。加了 lock-scroll 也没用,甚至把弹窗内容换成一句话(内容很短)后穿透更明显。折腾了好几轮,最后发现根因完全不在弹窗本身。
现象
- 背景穿透:popup 打开后,手指在遮罩区或 popup 内向上滑,背景列表跟着滚
- 短内容必现:弹窗内容不足 max-height 时,穿透 100% 复现
- 长内容真机不可滚:换成长列表后,popup 内容在模拟器(鼠标滚轮)能滚,真机(手指触摸)却完全无响应
lock-scroll默认已开,设不设没有区别
根因
翻 vant-weapp 源码,van-popup 的 lock-scroll 实现只有一行:给内部的 <van-overlay> 节点注册 catch:touchmove="noop"。
这意味着它只能拦截用户手指直接落在半透明遮罩区的滑动,不对 page 本身做任何处理。
当弹窗内容较短、用户手指落在 popup 白色区域内上滑,touch 事件冒泡路径是:popup 内容 → van-popup 容器 → page——整条路上没有任何 catch 拦截,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;' : '' }}" />三层分工明确:van-overlay 拦截遮罩区触摸 / scroll-view 原生消化弹窗内滑动 / page-meta overflow:hidden 兜底锁死 page 本身。每层独立,不存在事件链交叉,iOS 真机完全兼容。
<!-- 错误写法 -->
<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 背景穿透或内容不可滚,按顺序检查:
- 全文 grep
page-meta,确认存在且 page-style 动态绑定了 popup 状态 - grep
catch:touchmove,确认 van-popup 父链上没有任何 catch - 弹窗内容容器用的是
<scroll-view>而非<view style="overflow-y:auto"> - scroll-view 高度链完整:容器有
flex:1; min-height:0 - 多弹窗场景:page-meta 条件是否 OR 覆盖了所有 popup 的 show 状态
一句话外卖
van-popup 的
lock-scroll只是遮罩的局部补丁,锁住整页滚动必须靠page-meta overflow:hidden;弹窗内可滚动区域必须用<scroll-view>,任何catch:touchmove在其祖先上出现都会让 iOS 真机滚动失效。