Web 平台 API / iframe 的尺寸 · 让内容决定高度 / postMessage 旧方案与渐进增强 待审核 4 / 4
postMessage · ResizeObserver

postMessage 旧方案与渐进增强

frame-sizing 之前的十年里,「iframe 按内容定高」一直由脚本完成。Chrome 154 之外的浏览器至今仍然只能这样做,所以旧方案不会马上退场,它会作为回退路径与新方案并存(核对于 2026-09)。

1 · 旧方案的写法

内嵌文档量出自己的高度,发消息给外层;外层收到后改 iframe 的 height:

// 内嵌文档
new ResizeObserver(() => {
  const height = document.documentElement.getBoundingClientRect().height;
  parent.postMessage({ type: 'iframe-height', height }, 'https://blog.example');
}).observe(document.documentElement);

// 外层
addEventListener('message', (e) => {
  if (e.origin !== 'https://widget.example') return;
  if (e.source !== frame.contentWindow) return;
  if (e.data?.type !== 'iframe-height') return;
  frame.style.height = `${Math.ceil(e.data.height)}px`;
});

ResizeObserver 让它比早年轮询 setInterval 的写法精确得多:内容一变就通知,不变就安静。它还有一个 frame-sizing 没有的优点,高度是实时同步的,内嵌文档不需要记得在每次变更后调用什么。

两处安全检查不能省。内嵌文档的 postMessage 第二个参数应写外层的确切 origin 而不是 '*',否则任何嵌入它的页面都能收到高度,这正是 双向同意 §1 所说的信息泄露,旧方案要靠作者自觉堵住。外层要同时核对 e.origin 与 e.source:前者防止别的站点伪造消息,后者在同一页面嵌了多个同源 iframe 时分清是哪一个。

2 · 视口单位引起的循环

旧方案没有锁定 ICB 这一层保护。内嵌文档里只要有一个 height: 100vh 的元素,循环就会出现:外层把 iframe 改高,100vh 随之变高,<html> 的高度变大,ResizeObserver 再次触发,外层再改高。

图 2-1 左右两侧加载同一份内容,左侧走旧方案,右侧走 frame-sizing 并在 ResizeObserver 里调用 requestResize()。在 200px 内容前插入 100vh 块,左侧在 252ms 内收到 10 条消息,高度一路增长,直到撞上外层设的 2000px 上限才停;右侧停在 350px,即锁定的 150px 加 200px,之后不再变化(机制见 requestResize() §3)。

图 2-1 · 同一份内容在旧方案(左)与 frame-sizing(右)下的高度。可同步追加、删除内容或插入 100vh 块,观察左侧的循环与右侧的锁定。

另一处分歧在内容删空时。左侧量的是 <html> 自身的高度,内容删光后 iframe 塌成 0;右侧量的是滚动区域,下限是锁定的视口,停在 150px。旧方案因此能做出任意矮的 iframe,frame-sizing 要借助外层的初始高度才行(requestResize() §2)。

旧方案的循环没有通用解法,常见的补丁都各有代价:外层设一个高度上限(图 2-1 的做法,内容真的很长时会截断);内嵌文档改量 document.body.scrollHeight 之外的某个稳定元素(要求作者知道哪个元素不受视口影响);在消息里比较新旧高度、忽略小于若干像素的变化(只能减缓,不能消除逐级增长)。

3 · 两套方案的对照

维度 postMessage + ResizeObserver frame-sizing
浏览器支持 所有现代浏览器 Chrome 154 起(核对于 2026-09)
外层需要 一段 message 监听脚本 一条 CSS 声明
内嵌文档需要 一段 ResizeObserver 脚本 一条 <meta>,变化时调 requestResize()
同步时机 实时 加载时两次,此后按需
隐私边界 靠作者写对 targetOrigin 浏览器强制双向同意与 allow-origins
100vh 内容 循环增长 锁定 ICB,不增长
最小高度 可以为 0 首次布局时的高度
外层脚本失效时 退化为默认 150px 不受影响

4 · 渐进增强的写法

两套方案可以叠在一起:支持 frame-sizing 的浏览器用它,其余浏览器走 postMessage。关键是两边都要按同一个判据分流,否则同一次变化会被处理两次。

外层用 CSS 声明新方案,只在不支持时才监听消息:

iframe.embed {
  frame-sizing: content-height;
  height: auto;
}
if (!CSS.supports('frame-sizing', 'content-height')) {
  addEventListener('message', (e) => {
    if (e.origin !== 'https://widget.example' || e.source !== frame.contentWindow) return;
    if (e.data?.type === 'iframe-height') frame.style.height = `${Math.ceil(e.data.height)}px`;
  });
}

内嵌文档同时写 <meta> 与脚本,脚本按 requestResize 是否存在决定走哪条路:

<meta name="responsive-embedded-sizing" content="allow-origins=https://blog.example">
<script>
  const EMBEDDER = 'https://blog.example';
  new ResizeObserver(() => {
    if ('requestResize' in window) {
      try { window.requestResize(); return; } catch (e) { if (e.name !== 'NotAllowedError') throw e; }
    }
    const height = document.documentElement.getBoundingClientRect().height;
    parent.postMessage({ type: 'iframe-height', height }, EMBEDDER);
  }).observe(document.documentElement);
</script>

requestResize() 抛出 NotAllowedError 时(比如 allow-origins 没有列出当前外层)落到 postMessage,外层若不支持 frame-sizing 就会按消息调整。回退路径保留了旧方案的全部弱点,内嵌文档仍应避免视口单位。

这套写法里 ResizeObserver 调用 requestResize() 的频率与旧方案发消息的频率相同。requestResize() 每次都会强制一次同步布局,内容频繁变化的组件(比如实时聊天)可以把调用合并到 requestAnimationFrame 里,一帧最多一次。