Skip to content

接口成功后页面仍不对:H5 的数据、图片与布局就绪 ​

三个看起来毫不相关的 H5 问题:

  • 接口已经返回了数据,列表却怎么也滑不动;
  • 二维码已经拿到了,生成的海报里却没有它;
  • 页面一直在等定位授权,用户以为查询功能坏了。

它们有一个共同点:把不同任务的“完成”当成了同一个时刻。

本文根据两个 Vue 2 移动 H5 项目近期已提交的修复整理,核对日期为 2026-10-23。案例和示例已经脱敏;以下讨论来自源码,真机和业务回归清单是建议,不是本次的执行结果。

读完这篇你会

  • 分清数据就绪、DOM 更新、资源加载结束和布局测量完成五个不同时刻
  • 在图片加载、弹层显示后正确地重新测量滚动区域
  • 区分“等待结束了”和“资源都成功了”
  • 判断定位这类辅助信息什么时候可以放到后台补充

1. 原理:家具送到了,不等于房间布置好了 ​

搬家的时候:

  • 家具运到了楼下(数据就绪:接口返回了数据);
  • 家具搬进了房间、摆到了位置上(DOM 就绪:框架完成了节点更新);
  • 窗帘拉开了、灯打开了(容器可见:弹层或页面显示出来了);
  • 墙上的装饰画陆续送到、挂好了,或者有一幅坏在了路上(资源结束:图片加载成功或失败);
  • 最后量一下房间还剩多少空间(布局测量:滚动组件取得当前的尺寸)。

如果在装饰画还没挂上的时候就量了尺寸,后来挂上的画就会让这个测量结果过时。

状态已经发生了什么还不能保证什么
数据就绪接口返回了可用的数据列表节点已经出现
DOM 就绪框架完成了本轮节点更新图片、字体已经加载
容器可见弹层或页面已经显示尺寸不会再变化
资源结束图片成功、失败或等待超时所有必需的资源都成功了
布局测量完成滚动组件取得了当前尺寸后续的资源不会再改变尺寸

Vue 2 的 $nextTick() 等待的是数据变化后的 DOM 更新,不会帮你等待网络图片。这正是需要在 DOM 更新之后,继续处理图片事件的原因。Vue 2 异步更新队列

2. 坑一:列表滑不动——先检查有没有真正的滚动容器 ​

现象:一个商品列表,内容一多就无法滚动,底部的商品看不到。

原因:列表原来放在一个固定高度的页面里,内容变长后,没有对应的滚动区域。

修复:近期的修复把列表放进了项目已有的滚动组件,为容器设置了完整的高度,并把底部的操作区放在滚动内容之外。

这类问题不能只靠多调用几次 refresh() 来解决。应该先确认:

  • 外层的高度是否有限、可以计算;
  • 内容是否真的超过了容器的高度;
  • 用的是原生滚动还是组件模拟的滚动;
  • 固定在底部的按钮有没有盖住最后一项;
  • 有没有父层同时截获了触摸或滚动事件。

这个页面仍然保留了“固定延时后刷新”的实现,它是已有修复的一部分,但不应该被理解成“等待若干毫秒就能保证布局完成”。

3. 坑二:图片和弹层让之前的测量失效 ​

现象:另一个项目已经用了滚动组件,但页面打开时内容只能滑动一半;或者弹层第一次打开时滑不动,关掉再打开就好了。

原因:滚动组件初始化时测量了一次高度,但当时图片还没有撑开内容,或者弹层还是隐藏的(隐藏元素的尺寸是 0)。

修复:近期的修复分别在这些时刻补上了重新测量:教程图加载完成、富文本里的配图加载、轮播图加载、弹层显示之后。部分图片入口同时处理 load 和 error,避免一张加载失败的图片让布局永远停留在旧状态。

text
数据写入 → DOM 更新 → 初次测量
                         ↑
图片结束 / 弹层显示 / 内容展开 → 再次测量

坑三:在父元素上监听图片的 load,没有触发 ​

现象:为了处理富文本里动态出现的图片,在外层容器上监听 load 事件,结果一次都没有触发。

原因:普通的 load 事件不会冒泡。当前代码在外层使用捕获阶段监听(addEventListener('load', handler, true)),才能接收到子图片的加载事件。不能把一个普通的父级监听器当成所有子图片都能触发的入口。MDN load 事件

坑四:迟到的图片把阅读位置拉回了顶部 ​

现象:用户读一篇长公告读到一半,一张图片加载完成,页面突然跳回了开头。

原因:图片加载的回调里,除了重新测量,还调用了“滚动到顶部”。

修复:重新测量和回到顶部是两件事。当前公告页面的图片回调只刷新滚动边界,不重置滚动位置。

同一帧里有很多图片完成加载时,建议合并测量,而不是每次都重新计算。下面是教学示例,并不代表当前项目已经采用:

js
function createLayoutRefresh(nextTick, measure) {
  let frame = 0;
  let disposed = false;
  return {
    refresh() {
      nextTick(() => {
        if (disposed || frame) return;
        frame = requestAnimationFrame(() => {
          frame = 0;
          if (!disposed) measure();
        });
      });
    },
    dispose() {
      disposed = true;
      cancelAnimationFrame(frame);
    }
  };
}

使用时要在组件销毁时调用 dispose();页面隐藏后的恢复策略由页面决定,不能靠持续的测量代替生命周期管理。

4. 坑五:海报里缺图——“等待结束”不等于“资源完整” ​

现象:生成的分享海报偶尔缺少二维码或者商品图,但界面提示“生成成功”。

修复进展:近期的海报修复把资料、二维码和图片的处理放到了渲染之前,等页面显示后再截图,并为接口失败提供了重试入口。

这里仍有一个重要的取舍:图片预加载失败,也会结束等待,而且整体等待有超时兜底。所以源码只能证明“等待有出口”,不能证明“生成时所有图片都是完整的”。

建议按业务价值给资源分类:

资源适合的失败策略
决定海报用途的二维码没有加载成功时阻止导出,保留重试
主要的内容图片按需求显示失败,或者明确的占位
装饰性的背景可以降级,但不能盖住内容

同时区分“数据加载失败”“图片不完整”“截图失败”和“上传失败”。一个统一的 loading 或“成功”提示,会掩盖具体卡在了哪一步。

5. 坑六:等定位授权,卡住了首屏 ​

现象:进入查询页后,页面一直显示加载中,直到用户在弹窗里点了“允许定位”才出结果;用户拒绝或者没注意到弹窗,页面就一直是空的。

修复:近期的查询页改为:有已有的坐标就先用它查询,没有坐标也先查询;定位在后台继续进行,后续的查询再使用定位结果。这样授权弹窗就不再阻塞首屏的数据。

当前实现按模块共享一次定位状态,没有自动重试或位置有效期的刷新。用户移动了位置、授权失败后又重新允许、长时间停留在页面上等场景,还需要单独设计。

这种做法只适合坐标是辅助信息的查询。如果定位决定了可以看到哪些数据、价格或者业务资格,缺少定位时继续执行,就可能改变结果的含义,不能直接照搬。

6. 坑七:兼容补丁执行得太晚 ​

现象:为了修复旧滚动库在某些设备上的问题,加了一段补丁代码,但在部分设备上不生效。

原因:这个依赖在加载时就确定了设备能力;组件注册时又会复制方法。补丁必须早于依赖读取运行,针对组件定义的修正必须早于组件注册。

项目还修正了一个版本比较的问题:原来只同时比较主版本号和次版本号,导致较高的主版本被漏判;修正同时扩展到了内部嵌套的滚动组件。临时补丁会尊重页面上的显式配置,避免覆盖已有的例外。

这说明兼容代码需要明确它针对的依赖版本和执行时机。它不是建议所有应用都去伪造触摸属性或统一关闭动画;升级依赖之后,要重新确认补丁是否还需要。

排查思路 ​

  1. 滑不动:在 DevTools 里查看滚动容器的高度和内容的高度,确认是否真的存在一个“内容比容器高”的滚动区域。
  2. 测量过时:在图片的 load / error 和弹层显示时打日志,看重新测量是在这些事件之前还是之后。
  3. 海报缺图:记录每一项资源的加载结果(成功、失败、超时),而不只记录“等待结束”。
  4. 真机验证:触摸、嵌套滚动、定位授权,都要在真实目标设备上测试,不能只用桌面模拟。

建议验证 ​

  • [ ] 缓存命中、慢图片、图片失败、没有图片时,内容都能完整滚动
  • [ ] 图片在用户阅读中途加载,测量更新了,但滚动位置没有被重置
  • [ ] 隐藏的弹层第一次显示和再次显示,都有正确的滚动边界
  • [ ] 关键的海报资源失败或超时,不会误报“生成完整”
  • [ ] 定位被拒绝、超时、后来成功,查询行为都符合业务约定
  • [ ] 在真实目标设备上验证触摸、鼠标和嵌套滚动

延伸阅读 ​

为复用而记录,为理解而整理。