Web 平台 API / iframe 的尺寸 · 让内容决定高度 / requestResize() 与锁定的 ICB 待审核 2 / 4
requestResize · locked ICB

requestResize() 与锁定的 ICB

双向同意 §4 说过,内嵌文档只在 DOMContentLoaded 之后的第一次布局与 load 时报一次尺寸。评论区追加了一条回复、折叠面板被展开,iframe 都不会跟着变。window.requestResize() 让内嵌文档在这些时刻主动再报一次。

1 · 调用条件

requestResize() 挂在 Window 上,规范的算法依次检查三件事,任何一件不满足都抛 NotAllowedError:

  • 当前窗口是一个子 navigable,即它确实被嵌在别的文档里。顶层页面调用会抛错,实测一致
  • 承载它的元素是 <iframe>,<object>、<embed> 与 <frame> 都不行
  • 文档的 responsive embedded sizing flag 为真,即首次解析时遇到了有效的 <meta>

条件都满足时,浏览器先执行挂起的样式与布局计算,再把宿主 iframe 的 internal layout intrinsic size 设为内嵌文档滚动区域的宽高。执行挂起的布局意味着它是一次强制同步布局,在一个事件处理里修改 DOM 后立即调用,代价与读取 offsetHeight 相同。

旧浏览器上这个方法不存在,未同意的文档上它抛错,两种情况都不该让页面报错。推荐的调用写法是:

function resizeHost() {
  if (!('requestResize' in window)) return;
  try {
    window.requestResize();
  } catch (e) {
    if (e.name !== 'NotAllowedError') throw e;
  }
}

2 · 增删内容的实测

图 2-1 的 iframe 已完成双向同意,初始内容 240px。实测序列如下:

  • 追加 80px 并调用:iframe 从 240px 变为 320px
  • 改为不调用,再追加 80px:内嵌文档的 scrollHeight 已是 400px,iframe 仍是 320px,内部出现滚动条
  • 手动调用一次:iframe 变为 400px
  • 逐块删除并调用:400 → 320 → 240,最后一块删除后停在 150px 而不是 0

最后这个 150 不是巧合。尺寸取自滚动区域,而滚动区域不会小于视口;内嵌文档的视口又在首次布局时被锁定(§3),锁定的正是 iframe 当时的高度,默认就是 150px。按内容定高的 iframe 因此有一个隐含的下限:它最矮就是首次布局时的高度。

这个下限可以由外层控制。实测外层先把 iframe 写成 height: 0,内嵌文档加载完成后再改回 height: auto,20px 的内容得到 20px 高的 iframe;随后把内容改为 400px 并调用 requestResize(),iframe 变为 400px;再改为 10px,iframe 缩到 10px。初始高度写 40px 时,下限就是 40px。代价是外层需要一段脚本在 load 后切换高度,加载完成前 iframe 不可见;对一行提示条这类比 150px 矮的嵌入,这是目前能得到精确高度的写法。

图 2-1 · 一个已双向同意的 iframe,内嵌文档可追加、删除内容或插入 100vh 块。可切换变更后是否自动调用 requestResize(),观察 iframe 高度、内嵌 scrollHeight 与 innerHeight 的关系。

3 · 锁定的 ICB

按内容定高有一个天然的循环风险。内嵌文档里若有一个 height: 100vh 的元素,它的高度取决于 iframe 的高度;iframe 按内容变高,100vh 随之变高,内容又变高,iframe 再变高,永无止境。postMessage 旧方案 §2 会实际演示这个循环。

规范的对策是锁定 initial containing block(ICB):flag 为真的文档在第一次布局时记下当时的 ICB 尺寸,作为 locked embedded ICB size,此后计算尺寸时一律用它,而不再按 iframe 的当前尺寸重算。规范的注释直说这是为了降低布局循环的可能。

实测给出了这条规则在 Chrome 里的具体形状。在 240px 内容前插入一个 100vh 块并调用 requestResize(),iframe 变为 390px,即锁定的 150px 加 240px;100vh 在尺寸计算里按 150 算。但 iframe 变成 390px 之后,内嵌文档的 innerHeight 读到 390,100vh 块实际渲染为 390px,scrollHeight 变成 630px。此时再调用 requestResize(),iframe 仍是 390px,不再增长。

也就是说,锁定只作用于报给外层的尺寸,不作用于内嵌文档自己的渲染。循环被切断的代价是:含视口单位的内嵌文档,内容会比 iframe 高出一截,重新出现内部滚动条。按内容定高的内嵌文档应当避免 vh、dvh 一类视口单位,全屏式的布局本来也不适合放进一个自适应高度的 iframe。

规范另有两条边界:内嵌文档导航到新页面时,锁定的 ICB 随之失效,新文档重新计算;规范正文里还留着一个待定问题,是否需要一种办法让 iframe 主动忘掉锁定值,比如把 frame-sizing 关掉再打开。这一点本页没有设计出能区分两种行为的实测,暂不下结论。