本文目录11 节

一个看起来像 CSS 的问题

在一次首页模块改版中,我为不同推荐区域拆分了新的展示组件:有的使用横向角色卡片,有的需要上下交错排列,还有的增加了贴近图片底部的装饰元素。

浏览器中的初步效果正常,但放到 iOS 真机后,两个新模块的底部内容会被下一块模块遮挡。其他旧模块没有问题,因此第一反应很容易落到普通 CSS 上:是不是层级不够、父元素裁切了内容,或者安全区没有算对?

这些现象确实存在,但它们不是完整根因。真正的问题是,页面使用了虚拟列表,而虚拟列表理解的“这一项有多高”和组件实际渲染出来的高度已经不一致。

新旧模板先通过布局变体隔离

这次首页后端返回的模块仍然沿用原有 type,但同一个 type 在新版首页中可能对应完全不同的视觉结构。为了不把矩形角色卡、交错横滑和 2×3 网格都塞进旧组件,我在数据进入展示层时补充 _layoutVariant

for_you_feed   -> for_you_showcase
new_feed       -> new_arrivals_stagger
trending_feed  -> grid_2x3
popular_feed   -> grid_2x3
fine_treasures -> fine_treasure_mosaic

模块识别优先使用稳定的 id 或 sourceId,title 只作为测试阶段的兜底。这样既能让后端协议尚未完全稳定时继续真机验证,也不会把展示标题变成长期接口契约。

新版 For You 和 New Arrivals 分别拆成独立组件。旧组件服务于圆形头像横滑和两行跑马灯,如果继续复用,会让同一个文件同时承担两套差异很大的 DOM、尺寸和交互规则。拆分组件不是为了追求文件数量,而是让新旧布局的高度计算互不污染。

虚拟列表看到的是占位高度

普通文档流会根据 DOM 的真实尺寸把后续内容向下推。虚拟列表为了只渲染可见区域,需要提前知道每一项的位置,因此通常依赖配置高度或缓存高度计算下一项的起点。

可以把它理解为两套同时存在的坐标:

  • 真实内容高度:浏览器布局后,用户实际看到的组件尺寸;
  • 虚拟占位高度:虚拟列表用来计算下一项位置的尺寸。

当新组件的内容超过配置高度时,组件本身仍然可以继续绘制,但下一项已经按照较小的占位高度提前出现。于是视觉上就像“下一块压了上来”。

这也解释了为什么单纯提高 z-index 没有意义:问题不是谁盖在谁上面,而是下一项从一开始就被放在了错误的位置。

几个让高度失真的细节

固定 height 太相信设计稿

设计稿中的固定高度描述的是某个画布上的最终结果,不一定适合直接作为 WebView 中的布局约束。字体渲染、设备宽度、安全区和像素取整都可能让真实内容多出几像素。

新模块最初使用的固定高度过紧,内部稍有变化就会超出容器。将外层改为 min-height 后,组件仍然保留最低视觉尺寸,同时允许内容决定最终高度。

overflow: hidden 把线索藏起来

纵向裁切会让超出部分直接消失,看起来像是子元素尺寸不对。排查阶段将纵向 overflow 调整为可见,可以先确认内容到底有多高,再决定哪里应该裁切。

横向滚动和纵向裁切也不应该被一个笼统的 overflow 同时控制。这个问题让我更习惯按方向明确布局意图。

transform 不参与文档流

上下交错的卡片最初通过 transform: translateY(...) 下移。它在视觉上移动了,但浏览器计算父容器高度时仍然使用变换前的位置。

因此,卡片看起来已经更低,虚拟列表却不知道这段额外距离存在。改用 padding-top 后,下移量进入正常布局,父容器和虚拟占位才有机会得到一致结果。

判断标准很简单:

如果位移应该影响后续内容的位置,就不能只使用 transform。

边框与真机像素取整

半像素边框在高 DPR 屏幕上通常能得到更细的视觉效果,但也可能参与最终尺寸取整。给卡片统一设置 box-sizing: border-box,可以避免边框在预期尺寸之外继续撑大盒子。

单独看这类误差很小,可当固定高度、装饰定位和虚拟列表占位同时偏紧时,几像素就足以暴露问题。

修复不是只改一个数值

最终处理包含两层。

组件内部:

  • 固定高度改为最小高度;
  • 取消不必要的纵向裁切;
  • 使用参与文档流的间距实现交错;
  • 装饰元素放回每张卡片自己的视觉容器;
  • 统一盒模型,避免边框额外增加尺寸。

虚拟列表层:

  • 根据新的展示变体设置对应占位高度;
  • 为真机字体和像素取整保留少量安全空间;
  • 不影响仍使用旧布局的其他模块。

后续补齐 2×3 网格、1×4 横滑、1+4 组合和交错横滑模板时,我仍然只让这些新变体使用设计稿要求的 2px 模块间距,旧模板继续保留原来的 5px。CSS 间距和 _fixedHeight 必须同步修改,否则组件看起来只有 2px 间隔,虚拟列表却仍按旧高度安排下一项,最终真机结果仍会偏离设计稿。

这两个层次必须一起修改。只放大占位高度会掩盖组件自身的布局问题;只修组件内部,虚拟列表仍可能继续使用旧缓存或旧配置。

真机验证关注什么

完成修改后,我在 iOS 真机上逐项检查:

  • 新模块底部不再被下一项遮挡;
  • 上下交错仍然成立;
  • 装饰元素与图片底边保持对齐;
  • 横向滑动和卡片点击正常;
  • 后续旧模块的排列没有被新高度污染。

这里不能只依赖桌面浏览器截图,因为虚拟列表、WebView 字体和 DPR 取整共同参与了结果。

留下的排查方法

以后遇到虚拟列表中的重叠、空白或跳动,我会同时检查四个数字:组件真实高度、虚拟占位高度、视觉位移和盒模型额外尺寸。

我也不再把“看起来移动了”等同于“布局知道它移动了”。在虚拟列表中,视觉位置和计算位置一旦分离,后续所有项目都会基于错误坐标继续排列。先让这两套坐标重新一致,比不断补 margin、z-index 或 overflow 更可靠。

Recommended

继续阅读

  1. Ionic 缓存 Tab 中,怎样找到当前真正的滚动容器前端与移动端 · 7 分钟
  2. Ionic 缓存页面里,URL 变化、页面显示和数据刷新为什么是三件事前端与移动端 · 9 分钟