微信小程序 scroll-view 锁滚失效:flex 链 4 件套不够用时,用 absolute inset:0 钉死
一个 C 端小程序的「个人中心」二级页,需求是整页禁滚、只让 scroll-view 内滚。翻遍几十篇教程,抄了标准的「4 件套」配置。打开模拟器——完美。发给同事在 iOS 真机上试——整个 scroll-view 容器可以被下拉,橡皮筋一弹,白底 webview 背景色就裸着漏出来了。
问题是:同一个项目里,有 3 个同结构的页面用完全一样的 4 件套都没问题。这个页面 1:1 抄过去,就是不行。
现象
page.json里"disableScroll": true.page-wrap { height: 100vh; display: flex; flex-direction: column; overflow: hidden; position: relative; }.page-scroll { flex: 1; min-height: 0; overflow: hidden; }.page-scroll-content { min-height: 100%; }
iOS 真机:用户手指按住页面任意位置往下拉,scroll-view 整个容器盒跟着移动,页面顶部出现 webview 默认白底色带。
根因
flex: 1 + min-height: 0 能正确设置 scroll-view 的尺寸,但「尺寸正确」≠「容器不会被拽动」。复杂页面里有两类额外干扰叠加在一起:
第一层:fixed/absolute 子元素溢出 viewport 撑破 flex 高度链。 页面里有一个隐藏的 <canvas type="2d"> 用于生成二维码,位置设成 top: 150%; left: 150% 推到屏幕外。iOS WebKit 会把这个节点的 bounding box 算进 page content height,导致页面实际可滚区 > 100vh,此时 disableScroll 无法完全阻止整体位移。
第二层:enhanced scroll-view 自带 native bounces 和 refresher。 页面使用了 <scroll-view enhanced>,而 bounces 默认为 true——当 scroll-view 滚到顶部/底部边界时,native 层会拽动整个 scroll-view 盒子,而不仅仅是内容。下拉刷新手势(refresher-enabled 未关闭)同理,会接管下拉动作并产生容器位移。
flex: 1 + min-height: 0 让 scroll-view sizing 正确,但无法阻止:
- iOS WebKit 因屏外 native 元素的 bounding box 撑大 page content height
- enhanced scroll-view 的
bounces在到达边界时把容器整体移位 refresher-enabled接管下拉手势产生容器位移
3 个兄弟页面之所以能 work,是因为它们没有屏外 canvas、没有 enhanced 配置,或没有多层 absolute 卡片——变量不同,结果就不同。
正解
第 1 步:scroll-view 脱离 flex 链,用 absolute inset:0 物理钉死
.page-wrap {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative; /* 必须,作为 absolute 子元素的锚点 */
}
.page-scroll {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0; /* inset:0 物理钉死到 .page-wrap 四边 */
overflow: hidden;
}删掉 flex: 1 和 min-height: 0。scroll-view 不再依赖 flex 计算尺寸,而是直接锚定到 .page-wrap 四边——无论 flex 高度链被什么撑破,容器位置都不会变。
第 2 步:enhanced scroll-view 关闭自身回弹
<scroll-view
class="page-scroll"
scroll-y
enhanced
bounces="{{false}}"
refresher-enabled="{{false}}"
show-scrollbar="{{false}}"
bindscrolltolower="getMore"
>
<!-- 内容 -->
</scroll-view>bounces="{{false}}" 关掉 enhanced 模式的 native 边界回弹;refresher-enabled="{{false}}" 关掉下拉刷新手势接管。两者缺一不可。
第 3 步:native 组件包零尺寸容器
<canvas type="2d"> 等 native 组件即使推到屏外,DOM 仍参与 page bounding box 计算。解法是把它包在零尺寸 clip 容器里,放在 .page-wrap 内部:
<view class="page-wrap">
<view style="position:absolute; width:0; height:0; overflow:hidden;">
<canvas type="2d" id="hiddenQr"></canvas>
</view>
<scroll-view class="page-scroll">...</scroll-view>
</view>.page-scroll改为position: absolute; top/right/bottom/left: 0,彻底脱离 flex 尺寸链- enhanced scroll-view 显式加
bounces="{{false}}" refresher-enabled="{{false}}" - 屏外 native 组件包零尺寸容器,消除对 page bounding box 的污染
反模式备忘
- 改背景色匹配 webview 白底:肉眼看不出色差,但 rubber-band 仍在,换深色背景立刻原形毕露。
- 加
catch:touchmove="noop":会把 scroll-view 的 native scroll 也一起杀掉,iOS WebKit touch session 所有权机制决定了你不能只锁外层不锁内层。 - 继续堆 page-meta / overscroll-behavior / 5 件套 / 6 件套:每加一件意味着没找到根因。absolute pin 之前能 work 的页面不需要它,能 work 的页面本来就不会到这里来。
一句话外卖
flex 链控制尺寸,absolute inset:0 控制位置;当 native 层会拽走容器时,只有 absolute pin 才能守住边界。