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)。
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 里,一帧最多一次。