返回博客

微信小程序锁滚三轮排查:Canvas 越界才是橡皮筋的真凶

一个小程序页面上线了「禁止整页下拉橡皮筋」的需求。参考项目里其他三个已经稳定的同类页面,把锁滚 4 件套原封不动抄过来,结果:iOS 上手指一划,橡皮筋照样弹出来。

排查花了三轮,最终定位到的根因和"少了哪个配置"毫无关系,而是一个被推到屏幕外的 canvas 元素。

现象

参考页面(三个):4 件套配置,完全锁住,iOS 下拉无响应。

问题页面:同样的 4 件套,iOS 仍然可以触发橡皮筋回弹。

锁滚 4 件套如下:

// page.json
{ "disableScroll": true }
/* page-wrap */
.page-wrap {
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
 
/* scroll 容器 */
.page-scroll {
  flex: 1;
  min-height: 0;
}
 
/* 内容区 */
.page-scroll-content {
  min-height: 100%;
}

根因

Round 1:照抄 4 件套,失败

第一轮认为是"抄漏了某一件",逐项核对,没找到差异,失败。

Round 2:错位归因,加 page-meta,仍然失败

注意到参考页面中有一个用了 <page-meta> 组件,而问题页面没有。于是把 page-meta 当作第 5 件套加进去——用户实测,仍然能下拉

坑:把「working 页有而 broken 页没有的东西」当作根因

这是典型的错位归因。三个参考页面都没有用 page-meta 也能锁住,说明 page-meta 根本不是必要条件。此时应该做的是:找 broken 页独有的特征,而不是找 broken 页缺失但参考页也缺失的东西。

Round 3:3 路 diff,找到 broken 独有特征

做了真正的 1:1 对比——broken 页 vs 三个 working 页的完整子树 diff。

差异一眼看出:broken 页的 .page-wrap 里有一个用于离线核销码生成的 <canvas type="2d">,定位写法是:

canvas#writeOffQrcode {
  position: absolute;
  top: 150%;
  left: 150%;
}

用意是把 canvas 推到屏幕外,视觉上不可见。但问题在于:

iOS WebKit 在计算页面 scroll box 高度时,会把 position:absolute 子元素的 bounding rect 纳入计算,无论该元素是否超出视口。top: 150% 意味着这个 canvas 把 .page-wrap 的 content height 撑到了 250vh,远超 100vh——CSS overflow:hidden 锁住的只是视觉溢出,scroll box 本身已经可以滚动,手指拖拽时就触发了橡皮筋。

正解

撤掉 page-meta(它不是根因,加了反而引入干扰),把需要藏起来的 canvas 从 page-wrap 子树里物理移出,改用 position:fixed 配合负坐标真正脱离文档流:

<!-- 把 canvas 提升到 page-wrap 的兄弟节点 -->
<view class="page-wrap">
  <!-- 正常页面内容 -->
</view>
<canvas
  type="2d"
  id="writeOffQrcode"
  class="offscreen-canvas"
/>
.offscreen-canvas {
  position: fixed;
  top: -9999rpx;
  left: -9999rpx;
  width: 200rpx;
  height: 200rpx;
  z-index: -1;
  pointer-events: none;
}
正解:native 组件必须物理脱出 page content 子树

canvas / map / video / textarea / input 这类 native 组件,用 position:absolute + top/left: 150% 推到屏外,DOM 仍在父容器子树内,iOS WebKit 依然把它的 rect 计入滚动高度

正确藏法有两种:

  1. position: fixed + top/left: -9999rpx + z-index: -1 + pointer-events: none
  2. 把节点提升为 page-wrap 的兄弟节点,物理脱出 content tree
调试方法:3+ 路 diff,找 broken 独有特征

遇到「抄了 sibling 还是不行」的问题,别急着叠加配置,先做 broken vs 3 个 working sibling 的完整子树 diff,列出 broken 页独有的元素和样式。两者的交集才是真嫌疑,而不是「broken 缺少但 working 也缺少的东西」。

一句话外卖

微信小程序里,absolute 定位的 native 组件(canvas/map/video)推到屏幕外并不能脱离 iOS WebKit 的 scroll height 计算——页面锁滚失效时先检查子树里有没有越界的 native 节点,而不是叠加 page-meta。