父页发消息前必须等待 iframe.load 事件完成,否则 contentwindow 可能为 null;targetorigin 绝不能用 '*',须严格匹配协议+域名+端口;子页收消息时须用 === 严格校验 event.origin;敏感操作需用户手势预热。

父页发消息前必须等 iframe.load 事件完成
直接在 iframe 插入 DOM 后立刻调用 postMessage,iframe.contentWindow 很可能还是 null——尤其跨域时,浏览器不会立即建立引用。此时调用静默失败,控制台无报错,调试时极难定位。
正确做法是监听 load 事件,且必须在设置 src(或 srcdoc)之后绑定:
const iframe = document.createElement('iframe');
iframe.src = 'https://widget.example.com/player.html';
iframe.addEventListener('load', () => {
// 此时 contentWindow 才可靠
iframe.contentWindow.postMessage({ type: 'init' }, 'https://widget.example.com');
});
document.body.appendChild(iframe);
- 动态创建的
iframe不能依赖插入顺序或延时setTimeout猜测加载完成时间 - 如果子页有重定向(如 HTTP → HTTPS),
load会触发多次,应只响应最后一次;可用iframe.dataset.loaded = 'true'做标记防重复 -
srcdoc内联内容同样需监听load,否则contentWindow不可用
targetOrigin 绝不能写 '*',哪怕本地开发也得写死协议+域名+端口
postMessage 的第二个参数 targetOrigin 是浏览器级过滤开关:不匹配就静默丢弃,连错误都不抛。填 '*' 在生产环境等于放弃校验,攻击者只要控制任意页面嵌入你的父页,就能伪造消息触发 video.play()、location.replace() 等敏感操作。
必须显式指定目标 origin:
- 子页是
https://player.example.com:8080/widget.html→targetOrigin写'https://player.example.com:8080' - CDN 地址如
https://d34gxw3jqlasaag.cloudfront.net/player.html→ 必须写完整域名,不能省略https://或端口(即使默认 443) - 开发环境用
http://localhost:3000可以,但上线前必须替换,CI/CD 流程里建议加检查脚本防止漏改
子页收消息时只信 event.origin,且必须用 === 严格比对
event.origin 是浏览器注入的只读字段,代表消息来源的实际协议+域名+端口,是唯一可信的身份标识。用 includes('example.com') 或正则模糊匹配,会被 https://evil-example.com 绕过。
子页监听示例:
window.addEventListener('message', (event) => {
// ❌ 错误:模糊匹配
// if (event.origin.includes('example.com')) { ... }
// ✅ 正确:严格比对,且支持多源白名单
const allowedOrigins = [
'https://shop.example.com',
'https://admin.example.com'
];
if (!allowedOrigins.includes(event.origin)) return;
// 后续处理 event.data
});
-
event.source不可信——攻击者可伪造 iframe 并调用postMessage,source就是它自己的window - Safari 对跨域
contentWindow.location.origin访问更严,部分版本返回null,所以子页不能缓存 origin,每次都要现场读取event.origin - 若子页是
about:blank或通过document.write动态写入,event.origin为字符串'null',需显式比对event.origin === 'null'
敏感操作(如 play、fetch、localStorage)必须绑定用户手势上下文
移动端 iOS/Android 和新版 Chrome 强制要求:video.play()、audio.play()、document.execCommand('copy') 等 API 必须由真实用户手势(click、touchstart)触发。通过 postMessage 来的指令属于程序调用,99% 会抛 NotAllowedError。
子页必须主动“预热”媒体能力:
- 首次
touchstart或click时调一次video.play().catch(() => {}),并resolve一个全局playReadyPromise - 后续所有
postMessage指令(play、seek、volume)都await playReadyPromise后再执行 - 父页可先发
{ type: 'check-ready' }探测,子页响应{ type: 'ready', canPlay: true },避免盲目重试
这个约束常被忽略——不是通信没通,而是浏览器策略卡在最后一步。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











