1. 为什么你迟早会和 iframe 跨域通信打交道
先说个场景:你辛辛苦苦搭了一个后台管理系统,某天业务方提了个需求——要把另一个团队开发的报表页面、数据看板或者第三方工具直接嵌到你的系统里。你一听,这简单,<iframe src="xxx">一行代码搞定。结果嵌进去之后发现,子页面想告诉父页面“用户点了保存”,父页面想通知子页面“刷新一下数据”,因为你用的是原生 DOM 操作或者普通的事件绑定,压根拿不到另一个窗口的引用,控制台还飘着一行跨域报错。
这就是 iframe 跨域消息通信要解决的问题。只要你的页面里有 iframe、embed、object、window.open 打开的窗口,并且这些窗口跟主页面不在同一个源(协议、域名、端口任意一个不同),你就绕不开 postMessage 和 message 事件这套方案。它是浏览器官方提供的、专门用于跨窗口、跨域传递数据的 API,比 JSONP、CORS 修改后端、代理转发那些方案更轻量,也更贴合“前端页面之间直接对话”的场景。
我做过的后台项目里,至少有三四次是死在“怎么让 iframe 内外互相通信”这个问题上,早期还试过改 document.domain、轮询 localStorage、window.name 各种土办法,最后都用 postMessage 重写才彻底解决。这篇就把它彻底讲透,从原理到写法,从父子双向通信到多窗口广播,再到真实的坑和排查思路,你能想到的场景基本都覆盖了。
适合谁看?前端开发、需要做集成页面的全栈工程师、做低代码平台或多系统聚合门户的同学,都值得收藏一份。
2. 先搞清楚 iframe 跨域到底卡在哪一步
2.1 同源策略:浏览器不给你的东西
要理解跨域消息通信,必须先理解一个前提:浏览器有同源策略(Same-Origin Policy)。它的意思是,来自同一个协议、域名、端口的页面之间,浏览器才允许相互访问 DOM、Cookie、localStorage 这些东西。比如https://admin.example.com和https://api.example.com,域名不同,算跨域;http://localhost:8080和http://localhost:9090,端口不同,也算跨域。
同源策略限制的是“页面脚本对另一个窗口内部内容的访问能力”。你可以把 iframe 想象成一个玻璃房子,你能看到里面的人在动(页面渲染出来了),但你伸手进去摸他的桌子(操作子页面 DOM),就会被玻璃挡住,这层玻璃就是浏览器安全策略。
所以在跨域 iframe 里,以下操作基本都会被浏览器拦下来:
- 父页面执行
iframe.contentDocument.getElementById(...)去读子页面节点 - 子页面直接访问
window.parent.document去改父页面内容 - 直接用
window.parent.xxFunction()调用对方的全局函数
我见过很多刚接手这类需求的同学,第一反应就是“我直接调对面页面的函数不就行了吗”。不行,浏览器不允许。跨域之后,你手里能拿到的只有对方窗口的window引用,但里面的属性和方法全部被锁死,你唯一能安全使用的“通道”就是 postMessage。
2.2 跨域场景有哪些
实际项目里,iframe 跨域通信的形态大概有这几种:
| 场景 | 父页面 | 子页面 | 是否能直接用 DOM 互访 |
|---|---|---|---|
| 同域父子 | a.example.com | a.example.com/child | 可以 |
| 子域不同 | a.example.com | b.example.com | 不行 |
| 不同域名 | example.com | another.com | 不行 |
| 协议不同 | https://a.com | http://a.com | 不行 |
| 本地调试 | localhost:8080 | localhost:3000 | 不行 |
后面四种场景,正常手段都走不通,postMessage 就是为它们准备的。
2.3 CORS 和 postMessage 不是一回事
很多人会混淆:我后端都配了 CORS 跨域,为什么 iframe 还是通信不了?
CORS 控制的是“浏览器向跨域服务器发起 AJAX 请求时,服务器允不允许这个请求被读取”。它解决的是页面和服务器之间的资源访问问题。postMessage 解决的是“两个独立窗口/文档之间的数据传递”问题。后端配了 CORS,ifrmae 页面照样不能直接操作父页面 DOM,因为这不是 HTTP 请求层面的问题,而是窗口间的安全隔离。理解这一点,你排查的时候就不会跑偏。
3. postMessage 和 message 事件核心机制拆解
3.1 postMessage 到底发了什么、发给谁
postMessage 的调用方式是目标窗口.postMessage(message, targetOrigin, transfer),注意,是“目标窗口”调,不是随便哪个窗口调。这里是最容易绕晕的地方。
举个例子,父页面要发给子页面,代码是:
// parent 页面里 const iframe = document.getElementById('myIframe'); iframe.contentWindow.postMessage('hello child', 'https://child.example.com');父页面拿到的是子页面窗口的引用iframe.contentWindow,然后在这个引用上调用 postMessage,消息才会发进子页面里。
反过来,子页面要发给父页面,代码是:
// child 页面里 window.parent.postMessage('hello parent', 'https://parent.example.com');子页面拿到父页面窗口的引用window.parent,同样在它上面调用 postMessage。
第二个参数targetOrigin很关键:它用来指定“这个消息只允许发送给哪个源”。如果目标窗口当前的源和你指定的源不匹配,浏览器会直接丢弃这条消息,不会真的发过去。如果你不关心目标窗口是什么源,可以写成'*',但安全上不建议这么干,后面会细说。
第三个参数transfer比较少见,它用于转移可转移对象(比如 ArrayBuffer 的所有权),多数场景用不上,知道有这个东西就行。
3.2 message 事件:消息到了怎么接收
发送只是第一步,接收的一方要靠监听message事件才能拿到数据。不管是父页面还是子页面,接收的代码长一样:
window.addEventListener('message', function(event) { console.log(event.data); console.log(event.origin); console.log(event.source); });event对象上有几个属性,你必须烂熟于心:
event.data:发送方传过来的数据,可以是字符串、对象、数组,甚至通过 structured clone 算法复制过来的复杂结构event.origin:发送方页面的源,格式是“协议 + 域名 + 端口”,这是你判断消息是否可信的核心依据event.source:发送方窗口的引用,如果要做回执、要回复消息,就靠它event.lastEventId:和服务端消息事件相关的属性,前端场景里很少用
很多人刚学时最大的困惑是:我在子页面里写的监听,浏览器怎么知道要把消息分给谁?答案是所有窗口都会收到 postMessage 派发的 message 事件,但你监听了、且消息来源合法,事件才在你的监听器里被处理。所以每个窗口里只管写好自己的监听逻辑即可。
3.3 为什么必须校验 event.origin
这是新手最容易忽略、老手最容易翻车的点。
试想一下,你如果不在接收端校验event.origin,任何网站都能往你的页面窗口里发消息。攻击者可以构造一个恶意页面,把你的页面嵌在 iframe 里,然后向外发一条伪造的 message,你的监听器接到后可能会执行一些危险操作——比如把用户数据发出去、改变页面状态、触发某个接口调用。
所以接收端的标准姿势是:
window.addEventListener('message', function(event) { if (event.origin !== 'https://parent.example.com') { return; } // 安全了,再处理数据 handleMessage(event.data); });白名单里可以是一个数组,当存在多个可信来源时逐个判断。这条习惯必须养成,我见过不止一次因为少写一个 origin 校验,测试环境一切正常,上线后被第三方恶意投递消息的案例。
4. 完整实操:父子窗口双向通信的落地写法
4.1 父页面发送消息给子页面
假设父页面是https://parent.example.com/admin.html,子页面是https://child.example.com/widget.html,父页面嵌了一个 iframe,要在用户点按钮时通知子页面“刷新报表数据”。
父页面代码:
<!-- parent.html --> <iframe id="reportFrame" src="https://child.example.com/widget.html"></iframe> <button id="refreshBtn">刷新子页面报表</button> <script> const iframe = document.getElementById('reportFrame'); document.getElementById('refreshBtn').addEventListener('click', function() { iframe.contentWindow.postMessage({ type: 'REFRESH_REPORT', payload: { force: true } }, 'https://child.example.com'); }); </script>子页面代码:
// widget.html 里 window.addEventListener('message', function(event) { if (event.origin !== 'https://parent.example.com') { return; } const msg = event.data; if (msg && msg.type === 'REFRESH_REPORT') { loadReportData(msg.payload.force); } });有两个细节需要注意。第一,我特意把消息做成了对象结构{ type, payload },而不是传裸字符串,这样后续要扩展消息类型时,不用改通信协议,加一个 type 值就行。第二,子页面必须在window上监听,不是 document 上,这两个地方容易混,但规范里 postMessage 的事件是分发给窗口对象的。
4.2 子页面回传消息给父页面
子页面操作完成后,需要告诉父页面“报表刷新成功,共拿到 200 条数据”。子页面这样发:
window.parent.postMessage({ type: 'REFRESH_DONE', payload: { count: 200 } }, 'https://parent.example.com');父页面这样接:
window.addEventListener('message', function(event) { if (event.origin !== 'https://child.example.com') { return; } const msg = event.data; if (msg && msg.type === 'REFRESH_DONE') { updateStatus('刷新成功,共 ' + msg.payload.count + ' 条'); } });到这里,一个最基础的双向通信闭环就走通了:父通过iframe.contentWindow.postMessage发给子,子通过window.parent.postMessage发回给父,两边各自在window上监听 message 事件,并严格校验event.origin。
4.3 页面加载时序:iframe 没加载完怎么办
这是“为什么我发了消息,子页面没反应”排行榜第一的原因。
页面加载是有时序的。你在父页面 DOMContentLoaded 时立刻调iframe.contentWindow.postMessage,可能子页面的脚本压根还没执行到挂监听那一步,消息发过去就石沉大海了。解决思路一般有三种:
第一种:父页面等 iframe 的 load 事件再发。用iframe.onload = function() { ... }来保证子页面资源加载完毕。但注意,load 只保证资源加载完,不代表子页面的 JS 监听已经执行,严格说还不完全可靠。
第二种:双方约定握手协议。子页面加载完主动给父页面发一条CHILD_READY消息,父页面收到后再发正式业务消息。这是我认为最稳妥的方案,因为子页面知道自己什么时候才真正准备好接收指令。
第三种:父页面用setTimeout延迟发送。这个只能用于临时调试,生产环境千万别依赖猜时间。
我实际业务里一直都是用握手协议,子页面挂好监听后立刻向父页面发送 READY 标志,父页面维护一个状态位,收到 READY 后才允许用户触发业务消息。
4.4 子页面刷新父页面数据:反向数据流设计
还有一种常见场景:子页面里做了增删改操作,要刷新父页面的列表。同样的原理,子页面发DATA_CHANGED消息,父页面收到后重新请求自己的接口数据。整个思路和 4.2 一模一样,只是消息语义不同。
这里想多说一句设计上的建议:用 postMessage 通信时,最好在项目里把消息类型集中定义在一个常量文件里共用,避免魔法字符串满天飞。比如单独建一个messageTypes.js,里面导出REFRESH_REPORT、REFRESH_DONE、DATA_CHANGED这些常量。两边项目是不同仓库的话,可以抽成 npm 包或者直接复制一份同名常量文件,否则消息类型写错一个字母,调试起来很痛苦。
5. 复杂场景:多 iframe 广播、回执确认、跨域传参
5.1 父页面给多个 iframe 广播消息
一个页面里嵌了三个不同业务线的 iframe,父页面想一次性通知它们“用户登录状态变了”或者“全局主题切换了”。每个 iframe 的引用不一样,那就循环遍历:
const frames = [ document.getElementById('frame1'), document.getElementById('frame2'), document.getElementById('frame3') ]; frames.forEach(frame => { frame.contentWindow.postMessage({ type: 'GLOBAL_THEME_CHANGE', payload: { theme: 'dark' } }, '*'); });这里 targetOrigin 用了'*',可能有人会问,这样子安全吗?关于targetOrigin用'*'的安全性,要分场景。往前看,*意味着不管目标窗口当前处于什么源,浏览器都会尝试把消息发过去。如果这些 iframe 都是同公司的页面,且内容可信,用*在功能上没问题。但是如果你能确定每个 frame 的确切源,还是建议分别指定准确 origin,多写几行代码换来的是更可控的消息投递。
另外有个细节:如果父页面嵌了多个 iframe,且这些 iframe 同源,那用document.querySelectorAll('iframe')遍历会更灵活,但要防止拿到隐藏的无关注入组件。
5.2 子页面如何拿到父页面的准确身份
子页面接收消息时,event.origin给的是发送方“当时的源”。如果父页面在开发环境是localhost:8080,测试环境是test.example.com,生产环境是example.com,那子页面的白名单就得配置多个来源。一条不够严谨的错误做法是直接event.origin.includes('example.com'),因为fakeexample.com也能通过 includes 判断,必须精确匹配完整源字符串,除非你对源的结构做了专门解析。
5.3 需要子页面处理完再回执怎么办
有些场景是父页面发了一个任务,要求子页面处理完再回传结果,类似 RPC 调用的语义。只靠单个 message 事件,父页面不知道该结果对应哪一次请求。这时候可以给每条消息加一个自增的requestId:
// parent 侧 let reqId = 0; const pendingMap = new Map(); function sendToChild(payload) { reqId++; const id = 'req_' + reqId; pendingMap.set(id, { resolve: null, timer: null }); return new Promise((resolve, reject) => { pendingMap.get(id).resolve = resolve; pendingMap.get(id).timer = setTimeout(() => { pendingMap.delete(id); reject(new Error('child 处理超时')); }, 10000); iframe.contentWindow.postMessage({ type: 'CALL_CHILD', requestId: id, payload }, 'https://child.example.com'); }); } // 监听子页面回执 window.addEventListener('message', function(event) { if (event.origin !== 'https://child.example.com') return; const msg = event.data; if (msg && msg.type === 'CALL_CHILD_RESULT') { const pending = pendingMap.get(msg.requestId); if (pending) { clearTimeout(pending.timer); pending.resolve(msg.payload); pendingMap.delete(msg.requestId); } } });子页面处理完数据后,把同一个requestId原样带回。父页面用 Promise 包裹整个过程,调用的地方直接await sendToChild({ action: 'getDetail' })就能拿到结果,代码可读性非常高。这套模式我建议在涉及“父子协同完成一个业务操作”的场景里直接抄。
5.4 多级嵌套 iframe 怎么通信
有时候页面层级不止两层:最外层页面 A 嵌了 B,B 又嵌了 C。A 想给 C 发消息怎么办?直接发不行,因为 A 拿不到 C 的 window 引用。
处理办法是逐层转发。A 把消息发给 B,B 收到后再转给 C;C 的响应也逐层往上传递。中转页面的逻辑很简单:收到消息后判断目标层级,决定是自己消费还是继续转发。
这种链路里要注意消息头加一个targetLevel或targetFrameId字段,避免每一层都误以为消息是发给自己的。另外,层级越深,延迟和出错概率越高,能用两层解决的不要设计成三层。
5.5 和 Vue3 项目嵌套 iframe 的搭配实践
热搜词里有vue3嵌套iframe,这确实是高频场景。Vue3 组件里操作 iframe 有个经典坑:如果v-if控制 iframe 的渲染,组件卸载再重新挂载后,旧的 iframe 引用就失效了,必须重新获取新引用。而且 iframe 的src如果是在:src上动态绑定的,页面加载完成后才能拿到contentWindow。
实际开发里,我通常会把 iframe 通信逻辑封装成一个 composable 或者一个工具模块,里面统一管理 iframe 引用、监听函数注册、销毁时移除监听,而不是散落在各个组件的生命周期函数里。组件卸载时记得:
onBeforeUnmount(() => { window.removeEventListener('message', handler); });不然你切了几次路由,就挂了 N 个监听器在 window 上,消息会被处理好几遍,出现“明明只发一次,子页面却刷新了三次”的诡异问题。
5.6 主页面直接调用 iframe 里的函数,到底行不行
这个问题在热搜里也很常见:主页面可以调用iframe的函数吗。
如果同域,理论上可以:iframe.contentWindow.someFunction()就行。但一旦跨域,对不起,调用会被浏览器拦截。这也是为什么大家最终都会落到 postMessage 方案上。
换一种思路:你想调用的“函数”,本质上是一个操作指令。在跨域场景下,你不需要真的拿到对方的函数引用,只需要发送一个消息告诉对方“请执行某个操作”,对方收到消息后自己调用自己的函数即可。理解这个换位思考,跨域通信就没什么神秘的了。
6. targetOrigin、安全校验与前端防御清单
6.1 targetOrigin 传'*'和具体 origin 的区别
targetOrigin是投递阶段的校验:浏览器只把消息发给当前源匹配的窗口。如果你写了具体源,且目标窗口当前源不匹配,消息直接被丢弃,这是一种很有价值的前置保护。
但要注意一点:我们发送消息时,targetOrigin写的是“发送方期望的目标源”。如果目标窗口在 iframe 内部发生了跳转,比如你嵌的是https://child.example.com,但用户在里面点了一个链接跳到https://evil.example.com,这时候你依然拿着旧的contentWindow引用去 postMessage,并指定targetOrigin为https://child.example.com,浏览器发现目标窗口已经不是这个源了,消息就不会被送达。这其实是保护机制在起作用,避免你的数据被恶意页面接收。
建议:凡是能明确目标源的场景,都不要用'*'。'*'只在“广播给多个不同源窗口”且数据不敏感的情况下使用。
6.2 接收端校验清单
接收端必须检查以下内容,缺一不可:
event.origin是否在白名单内event.data是否满足期望的结构(比如必须是一个对象且有 type 字段)- 如果消息会触发接口调用或 DOM 操作,最好再校验一下业务参数是否合法
我有一个习惯:统一封装一个isTrustedMessage(event, allowedOrigins)函数,所有监听器都先过这个函数,不符合直接return。这样即使以后新加监听器,也不会忘记校验 origin。
const ALLOWED_ORIGINS = [ 'https://parent.example.com', 'https://child.example.com' ]; function isTrustedMessage(event) { return ALLOWED_ORIGINS.includes(event.origin); } window.addEventListener('message', function(event) { if (!isTrustedMessage(event)) return; // 业务处理 });6.3 消息内容千万别直接用来拼 HTML
即使你校验了 origin,也不能保证消息内容是安全的。对方页面如果被 XSS 注入,它发出的消息可能就是恶意构造的。所以在父页面收到消息后,如果消息里的 payload 要插入到 DOM,一定要用textContent而不是innerHTML,或者至少经过转义处理。把 postMessage 收到的数据一律视作不可信输入,这条原则能帮你避开大部分安全漏洞。
7. 真实项目中的坑:排查思路与高频问题实录
7.1 消息发了,对方就是收不到
按顺序排查:
- 检查发送方调用的窗口引用对不对。父发子要用
iframe.contentWindow.postMessage,子发父要用window.parent.postMessage,反过来写全都白搭。 - 检查监听器是不是挂在 window 上,且已经执行到挂载代码。子页面脚本还没执行完就监听失败的情况很常见,尤其是把 script 写在了页面底部而 iframe 又加载太快时。
- 检查 targetOrigin 是不是写错了。如果写的是完整源,而对方窗口当前源有细微差别(比如多了 www、端口没写),消息会被丢弃。
- 打开控制台,在监听器里第一步就
console.log(event.origin, event.data),先确认消息是否真的到达了。
7.2 监听器触发了多次
大概率是你在多个地方(或者同一个地方反复调用)都注册了同一个 message 监听器,比如组件 created 里注册了,又在 mounted 里注册了一次,或者路由切换没有移除监听。处理方式:用具名函数而不是匿名函数,保证removeEventListener能精准移除;组件销毁时一定要移除。
7.3 子页面 received 到了,但父页面监听不到子页面的回复
子页面发消息时用的窗口引用写反了,或者targetOrigin和父页面当前源不一致。本地开发时父页面是http://localhost:8080,子页面发送方 targetOrigin 却写了http://127.0.0.1:8080,两个源看起来一样,实际上不匹配,消息被丢弃。这种“看起来一样”的坑最磨人,排查时把两端实际跑在哪个源打出来对比。
7.4 vue3 里 iframe 的 v-if 切换导致消息丢失
如果在 Vue3 中用了v-if="showFrame"来动态渲染 iframe,每次切换后 iframe 都是新创建的,旧的内容已经被销毁。消息发出去了,目标窗口已经不存在了。解决:不要缓存旧引用,每次创建 iframe 后用回调或 watch 等新引用稳定后再发消息;更稳的做法还是让子页面主动发送 READY 消息,父页面只向“已就绪的 iframe”发消息。
7.5 iframe 加载的第三方页面不允许被嵌入
热搜里提到的 dataease 社区版明确禁止通过 iframe 嵌入,这种问题不是 postMessage 能解决的,而是对方页面通过X-Frame-Options响应头(值是 DENY 或 SAMEORIGIN)或者 CSP 的frame-ancestors指令拒绝被嵌。你的 iframe 会直接显示一个空白页或者浏览器的“拒绝连接”页面。
遇到这种限制,要么联系对方开放白名单,要么在后端用代理转发方式拿数据再自己渲染(这已经脱离 iframe 通信的范畴,属于数据聚合思路了)。排查方法很简单:打开控制台看 Network 面板,找到 iframe 对应文档的响应头,确认有没有X-Frame-Options或Content-Security-Policy限制。
7.6 同域 iframe 反而“通信不了”
同域时理论上直接用 contentWindow 调函数就行,但我在项目里也见过有人同域还非要走 postMessage,结果因为过度封装反而出问题。同域场景下,postMessage 当然也可以用,浏览器允许你这么做,只是没必要。直接调函数、直接操作 DOM 效率更高,也更直观。要判断“到底是否同域”,在控制台跑一下window.location.origin和iframe.contentWindow.location.origin,一样就是同域。
7.7 iframe 隐藏滚动条和通信的关系
有人问 iframe 隐藏滚动条跟通信有什么关系?本身没关系,但如果你操作的是父页面上 iframe 标签的样式(scrolling="no"、CSSoverflow: hidden),这些操作在跨域下是可以做的,因为它属于父页面对自己内部元素样式的控制,不涉及访问子页面内部 DOM。容易混淆的点是:你能改 iframe 标签的宽高,不代表你能读 iframe 内部的内容高度。跨域下想根据子页面内容自适应高度,还是得靠子页面把高度数据通过 postMessage 传给父页面,父页面再动态设置 iframe 的高度。这是 postMessage 特别实用的一类场景。
7.8 跨域状态下操作父页面 DOM 或调用函数
跨域后,子页面访问window.parent.document会得到跨域错误,调用window.parent.someFunc()也不行。唯一能依赖的就是window.parent.postMessage和父页面的监听器。所以设计上要提前把“子页面需要父页面配合的动作”抽象成消息,而不是考虑能不能直接调函数。
8. 通信方案选型与拓展:从 postMessage 到更完善的消息体系
8.1 postMessage 和 localStorage、BroadcastChannel 怎么选
有些场景你可能会纠结,除了 postMessage,还有没有别的跨窗口通信方案?
| 方案 | 适用场景 | 跨域能力 | 局限 |
|---|---|---|---|
| postMessage | iframe、window.open 等窗口间通信 | 支持 | 需要双方各写监听和发送逻辑 |
| localStorage + storage 事件 | 同源多标签页 | 不支持跨域 | 同源才能共享存储 |
| BroadcastChannel | 同源多标签页/同源 iframe | 同源 | 跨域不行 |
| Cookie 轮询 | 子域相同场景 | 部分支持 | 改 document.domain、轮询有延迟 |
结论很明确:凡是真正跨域的 iframe 通信,postMessage 是唯一稳妥的内置方案。postMessage 配合 message 事件,能做到真正的双向、即时、结构化数据传输。同源场景下你可以按需选 localStorage 或 BroadcastChannel,但它们替代不了 postMessage 的跨域能力。
8.2 会话凭证跨域传递的思路
还有一个高频需求:父页面登录了,子页面怎么拿到登录态?如果子页面和父页面不同域,Cookie 默认不会自动带过去。常见做法是父页面通过 postMessage 把 token 或短暂的授权码发给子页面,子页面拿到后再自己调用后端接口换 session 或临时凭证。要在消息里加一个过期时间或者一次性标识,降低 token 泄露被重放的风险。这种场景下,postMessage 的安全要求更高,origin 校验、数据加密(比如用现有密钥对 token 做简单包装)都值得做。
8.3 要不要自己封装一套消息总线
当项目里父子通信的频次变高,就会出现几十种 message type,代码里到处是window.addEventListener('message'),维护成本开始上升。这时候可以考虑封装一个简单的事件总线:
class FrameMessenger { constructor(origin, getTargetWindow) { this.origin = origin; this.getTargetWindow = getTargetWindow; this.listeners = new Map(); this.handler = this.handleMessage.bind(this); window.addEventListener('message', this.handler); } send(type, payload) { const target = this.getTargetWindow(); if (!target) return; target.postMessage({ type, payload }, this.origin); } on(type, callback) { if (!this.listeners.has(type)) { this.listeners.set(type, []); } this.listeners.get(type).push(callback); } handleMessage(event) { if (event.origin !== this.origin) return; const msg = event.data; if (!msg || !msg.type) return; const cbs = this.listeners.get(msg.type); if (cbs) { cbs.forEach(cb => cb(msg.payload, event)); } } destroy() { window.removeEventListener('message', this.handler); this.listeners.clear(); } }使用起来就清爽了:两边各自 new messenger,业务代码里只关心send和on,不用再手写事件对象判断。我建议消息类型多到 10 种以上时就该做这层封装。
8.4 当 iframe 加载的是后端渲染页面或第三方报表
有些 iframe 内容不是传统前端页面,而是服务端渲染的页面、BI 报表、数据看板。只要页面里有 JS 环境,它就能用 postMessage。但如果对方是纯静态页面或者不允许你改源码,那你就没法在对方页面里写监听器了,这种情况只有一种办法:让对方的开发配合你加一段消息监听代码,否则任何前端技巧都无能为力。这也是为什么做集成平台时,和第三方页面的通信协议一定要提前约定。
最后再从实战角度说几句
回到最初需求,如果你要在系统里嵌入一个跨域的数据看板,并且要和它做联动,我的建议是先别急着写代码。把通信协议定好:有哪些消息类型、由谁先发起、消息的 payload 结构长什么样、错误怎么反馈。协议定了,postMessage 的编码只是体力活。我每次项目里这套协议稳定后,基本没再因为通信问题返工过。
另外一个小技巧,你调试 postMessage 时,可以在两边页面的控制台里分别输入:
window.addEventListener('message', e => console.log('收到', e.origin, e.data));不用刷新页面就能看到所有消息,排查问题时比单步调试好用得多。
这个内容后续还能扩的方向有不少,比如结合 MessageChannel 做更精细的双向通道、用 SharedWorker 做多页面消息中转、或者把 FrameMessenger 封装成跨项目共用的 npm 包。只要你想做跨窗口协作,postMessage 这套是绕不开的地基。