前端知库

iframe 有哪些优缺点?父页面和 iframe 之间怎样通信?

iframe 在当前页面里嵌入一个完整的独立文档,适合嵌第三方内容、做隔离;代价是每个 iframe 都要额外的内存和加载,跨域时脚本互相访问受同源策略限制。同源时可以直接通过 contentWindow 访问,跨域时用 postMessage 并校验 origin,长期通信可以用 MessageChannel。

参考答案

简答

页面里放一个地图、一段视频播放器或者一个支付表单,常用的就是 <iframe>。它在当前页面里嵌入另一个完整的文档,这个文档有自己的 window、document 和导航历史。

好处来自“独立”:被嵌的页面跟父页面的样式和脚本互不干扰;跨域的话,双方脚本还被同源策略隔开;配合 sandbox 属性可以进一步禁止脚本、表单、弹窗。所以嵌第三方内容、展示用户生成的 HTML、老系统接入新页面,都会用它。

代价也来自“独立”:MDN 提醒每个 iframe 都是一个完整的文档环境,需要额外的内存和计算资源;它的内容不会自动撑开高度;屏幕阅读器用户需要 title 才知道里面是什么;对方站点还可能用 X-Frame-Options 或 CSP 的 frame-ancestors 禁止被嵌入。

通信分两种情况。同源时,父页面通过 iframe.contentWindow 拿到子页面的 window,子页面用 window.parent 拿到父页面,可以直接调用。跨域时只能用 postMessage 发消息,发送时写明目标 origin,接收时检查 event.origin。

iframe 适合用在哪里

最常见的是嵌入别的站点的内容,比如地图、视频、广告、第三方登录或支付页面。对方的页面在自己的文档里运行,父页面不用关心它用了什么技术。

另一类是隔离不可信的内容。MDN 的示例里就有用 srcdoc 加 sandbox 展示一段 HTML 源码的写法,用来防止展示用户内容时被注入脚本。sandbox 为空时启用全部限制,再用 allow-scripts、allow-forms 这类令牌逐项放开。可是同源内容同时放开 allow-scripts 和 allow-same-origin 时,被嵌的页面能把 sandbox 属性自己删掉,等于没加,详见《iframe sandbox 的权限组合有什么风险?》。

iframe 的缺点有哪些

首先是开销。每个 iframe 都要加载并维护一个完整的文档,数量多了会占内存、拖慢页面。对不在首屏的 iframe,可以加 loading="lazy",等它接近视口再加载。

其次是尺寸。出于安全和隐私原因,iframe 默认不会把内容的尺寸告诉父页面,所以高度不会自动跟着内容变。常见的做法是让子页面量好高度,用 postMessage 告诉父页面去改 iframe 的高度。

还有无障碍和可嵌入性。MDN 建议给 iframe 加 title,否则屏幕阅读器用户得进到里面才知道是什么内容。另外,对方站点返回了 X-Frame-Options: DENY,或者用 CSP 的 frame-ancestors 限制了嵌入方,浏览器就不会在你的页面里显示它,见《怎样用 frame-ancestors 限制页面被嵌入?》。

最后是加载状态不可靠。MDN 说明,为了防止有人用 iframe 探测内网地址,浏览器不会在 iframe 上触发 error 事件,即使内容加载失败也会触发 load。所以不能靠 onerror 判断嵌入是否成功。

同源时怎样直接访问

父页面和子页面同源时,可以像访问普通对象一样互相访问:

JavaScript
// 父页面
const frame = document.querySelector('iframe');
frame.contentWindow.someFunction();
frame.contentDocument.body.style.background = '#fff';

// 子页面
window.parent.document.title = '子页面改了标题';

contentDocument 和 contentWindow.document 是同一个东西。可是只要不同源,这些属性里的大部分内容脚本都碰不到,contentDocument 会是 null,这是同源策略的限制。

跨域时用 postMessage

跨域时只剩 postMessage 这条路。它把消息用结构化克隆复制一份,在对方窗口上派发一个 message 事件。

JavaScript
// 父页面:第二个参数写子页面的确切源
frame.contentWindow.postMessage({ type: 'init', userId: 42 }, 'https://child.example');

// 子页面:先看消息是从哪来的
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://parent.example') return;
  if (event.data?.type !== 'init') return;
  event.source.postMessage({ type: 'ready' }, event.origin);
});

为什么发送时不写 '*'?因为 iframe 里的页面可能已经被导航到了别的站点,写 '*' 消息就会发给当前在那里的任何页面。写确切的源,源对不上浏览器就不派发。

为什么接收时要查 event.origin?因为 MDN 指出,iframe 层级里的任何窗口(包括恶意站点的)都可以给其他窗口发消息。确认来源以后,还要校验消息的格式,不能把收到的内容直接拿去执行或插进页面。更多细节见《跨窗口 postMessage 怎样避免把消息发给错误对象?》。

消息多了可以用 MessageChannel

postMessage 所有消息都走同一个 message 事件,类型一多就要自己区分。MessageChannel 能建一条专用通道,它有两个端口,一端留在父页面,另一端通过 postMessage 的第三个参数转交给 iframe,之后双方只在这条通道上收发。

下面在 node 里演示两个端口之间的一问一答,浏览器里的用法相同,只是 port2 会先被转交给子页面:

JavaScript
const { port1, port2 } = new MessageChannel();

port2.onmessage = (event) => {
  console.log('子页面收到:', event.data);
  port2.postMessage({ type: 'pong', id: event.data.id });
};
port1.onmessage = (event) => {
  console.log('父页面收到:', event.data);
  port1.close();
  port2.close();
};

port1.postMessage({ type: 'ping', id: 1 });
console.log('消息已发出');

用 Node.js 22 运行,输出依次是:

text
消息已发出
子页面收到: { type: 'ping', id: 1 }
父页面收到: { type: 'pong', id: 1 }

“消息已发出”最先打印,说明 postMessage 不会同步调用对方的处理函数,消息是异步送达的。转交给 iframe 时的写法是 frame.contentWindow.postMessage('init', 'https://child.example', [channel.port2]),端口转交以后父页面这边就不能再用 port2 了。

常见追问

子页面能把整个页面跳走吗?

同源的子页面可以设置 window.top.location。跨域的子页面只有在用户和它有过交互之后才能让顶层页面跳转,加了 sandbox 的 iframe 则需要 allow-top-navigation 或 allow-top-navigation-by-user-activation 才行。

同一域名下的多个标签页也能这样通信吗?

标签页之间拿不到彼此的 window 引用(window.open 打开的除外),一般用 BroadcastChannel 或 storage 事件,见《浏览器多个标签页之间怎样通信?》。