返回博客

微信小程序 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 链只管尺寸,管不住容器被 native 层拖走

flex: 1 + min-height: 0 让 scroll-view sizing 正确,但无法阻止:

  1. iOS WebKit 因屏外 native 元素的 bounding box 撑大 page content height
  2. enhanced scroll-view 的 bounces 在到达边界时把容器整体移位
  3. 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: 1min-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>
正解:absolute inset:0 + bounces/refresher false 三步组合
  1. .page-scroll 改为 position: absolute; top/right/bottom/left: 0,彻底脱离 flex 尺寸链
  2. enhanced scroll-view 显式加 bounces="{{false}}" refresher-enabled="{{false}}"
  3. 屏外 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 才能守住边界。