本文档系统讲解
postMessage的使用场景、API 用法、底层原理、安全实践与常见问题,帮助开发者掌握跨窗口、跨源、跨线程通信的核心能力。
一、什么是 postMessage
postMessage 是 HTML5 引入的跨源通信 API,用于在不同窗口、iframe、Worker、Service Worker 等上下文之间安全地传递消息。
它解决了传统跨域通信方案(如 document.domain、JSONP、代理页面)的安全隐患与功能局限,是现代前端跨上下文通信的标准方案。
1.1 核心能力
| 能力 | 说明 |
|---|---|
| 跨源通信 | 不同协议、域名、端口之间可安全通信 |
| 跨窗口通信 | 主页面与 iframe、弹窗、父窗口之间通信 |
| 跨线程通信 | 主线程与 Web Worker、Service Worker 之间通信 |
| 结构化数据传输 | 支持对象、数组、Map、Set、ArrayBuffer 等 |
| 转移所有权 | 可转移 ArrayBuffer、MessagePort 等,零拷贝 |
二、基本用法
2.1 API 签名
js
// 发送消息
targetWindow.postMessage(message, targetOrigin, [transfer])
// 接收消息
window.addEventListener('message', (event) => {
// event.data 发送的数据
// event.origin 发送方的源(协议+域名+端口)
// event.source 发送方的 window 引用
})
2.2 参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
message |
任意可结构化克隆的值 | 要发送的数据 |
targetOrigin |
string | 目标源,如 "https://example.com","*" 表示不限 |
transfer |
Transferable\[\] | 可选,转移所有权的对象(如 ArrayBuffer) |
2.3 事件对象属性
| 属性 | 说明 |
|---|---|
event.data |
接收到的数据 |
event.origin |
发送方源(如 https://a.com) |
event.source |
发送方的 window 引用,可用于回复 |
三、使用场景与示例
3.1 主页面与 iframe 通信
父页面发送给 iframe:
js
const iframe = document.getElementById('myIframe')
iframe.contentWindow.postMessage(
{ type: 'GREETING', payload: 'hello' },
'https://child.example.com'
)
iframe 接收:
js
window.addEventListener('message', (event) => {
if (event.origin !== 'https://parent.example.com') return
console.log('收到父页面消息:', event.data)
// 回复
event.source.postMessage({ type: 'REPLY', payload: 'world' }, event.origin)
})
3.2 iframe 向父页面通信
js
// iframe 内
window.parent.postMessage({ type: 'READY' }, 'https://parent.example.com')
// 父页面
window.addEventListener('message', (event) => {
if (event.origin !== 'https://child.example.com') return
if (event.data.type === 'READY') {
// 子页面已就绪
}
})
3.3 主线程与 Web Worker 通信
js
// 主线程
const worker = new Worker('worker.js')
worker.postMessage({ type: 'COMPUTE', data: [1, 2, 3] })
worker.onmessage = (event) => {
console.log('Worker 返回:', event.data)
}
// worker.js
self.onmessage = (event) => {
const result = event.data.data.map(x => x * 2)
self.postMessage({ type: 'RESULT', data: result })
}
注意:Worker 中
postMessage是全局方法,不需要targetWindow,也不需要targetOrigin(同源)。
3.4 MessageChannel 双向通信
MessageChannel 创建一对端口,可实现双向、独立通道通信。
js
const channel = new MessageChannel()
// 端口1 监听
channel.port1.onmessage = (event) => {
console.log('port1 收到:', event.data)
}
// 端口2 发送
channel.port2.postMessage('hello from port2')
// 将 port2 转移给 iframe
iframe.contentWindow.postMessage('init', 'https://child.example.com', [channel.port2])
典型用途:与 iframe、Worker 建立长期双向通道,避免全局 message 事件互相干扰。
3.5 Service Worker 通信
js
// 页面
navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' })
// Service Worker
self.addEventListener('message', (event) => {
if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
}
})
3.6 弹窗(window.open)通信
js
const popup = window.open('https://other.example.com')
// 等待弹窗加载完成后发送
popup.postMessage({ type: 'INIT' }, 'https://other.example.com')
// 弹窗内接收并回复
window.addEventListener('message', (event) => {
if (event.origin !== 'https://opener.example.com') return
event.source.postMessage({ type: 'ACK' }, event.origin)
})
四、底层原理
4.1 消息传递机制
postMessage 的通信流程:
发送方 接收方
│ │
│ 1. 调用 postMessage │
│─────────────────────────────>│
│ - 序列化 message │
│ - 校验 targetOrigin │
│ - 投递到接收方事件队列 │
│ │
│ │ 2. 触发 message 事件
│ │ - 反序列化 data
│ │ - 填充 origin/source
│ │
│ 3. 接收方处理 │
│<─────────────────────────────│
4.2 结构化克隆算法(Structured Clone)
postMessage 传输数据时使用结构化克隆算法,而非 JSON 序列化。
支持的数据类型:
- 原始值:string、number、boolean、null、undefined、BigInt
- 对象与数组(可嵌套)
- Date、RegExp
- Map、Set
- ArrayBuffer、TypedArray、DataView
- Blob、File、FileList
- ImageData、ImageBitmap
- Error 对象
不支持的类型:
- Function(函数)
- Symbol
- DOM 节点
- WeakMap、WeakSet
- Error 的某些子类属性
- 原型链(克隆后原型变为 Object.prototype)
与 JSON 序列化的区别:
| 特性 | 结构化克隆 | JSON |
|---|---|---|
| 支持 Map/Set | ✅ | ❌ |
| 支持 ArrayBuffer | ✅ | ❌ |
| 支持 Date | ✅ | ❌(转字符串) |
| 支持循环引用 | ✅ | ❌ |
| 支持函数 | ❌ | ❌ |
| 性能 | 较高 | 较低 |
4.3 转移所有权(Transferable)
对于 ArrayBuffer、MessagePort、ImageBitmap 等,可通过 transfer 参数转移所有权,实现零拷贝。
js
const buffer = new ArrayBuffer(1024 * 1024) // 1MB
worker.postMessage({ buffer }, [buffer])
// 转移后,主线程的 buffer 变为 detached,不可再用
console.log(buffer.byteLength) // 0
优势:
- 避免大数据的序列化与复制开销
- 适合传输大文件、图像数据、音视频帧
注意:
- 转移后原上下文失去访问权
- 只能转移可转移对象(Transferable)
4.4 事件队列与异步性
postMessage 投递的消息是异步的,会进入接收方的事件队列,在当前执行栈清空后处理。
js
window.postMessage('hello', '*')
console.log('先执行')
// 输出:先执行 → 收到 hello
4.5 源(Origin)校验
targetOrigin 参数在发送时校验:
- 若接收方源与
targetOrigin不匹配,消息不会投递 - 使用
"*"表示不限制,但存在安全风险
event.origin 在接收时校验:
- 接收方应主动检查
event.origin是否为可信源 - 不要仅依赖
event.source判断来源
五、安全实践
5.1 始终校验 origin
错误做法:
js
window.addEventListener('message', (event) => {
// 未校验来源,任何页面都可发送消息
doSomething(event.data)
})
正确做法:
js
const TRUSTED_ORIGINS = ['https://a.example.com', 'https://b.example.com']
window.addEventListener('message', (event) => {
if (!TRUSTED_ORIGINS.includes(event.origin)) {
console.warn('不可信来源:', event.origin)
return
}
doSomething(event.data)
})
5.2 发送时指定精确 targetOrigin
错误做法:
js
iframe.contentWindow.postMessage(data, '*') // 任何源都能接收
正确做法:
js
iframe.contentWindow.postMessage(data, 'https://child.example.com')
只有在确实无法确定目标源时(如开发调试),才使用
"*",且消息中不应包含敏感数据。
5.3 校验消息结构
js
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted.com') return
if (typeof event.data !== 'object' || event.data === null) return
if (event.data.type !== 'EXPECTED_TYPE') return
// 处理
})
5.4 避免在消息中传递敏感信息
- 不要通过
postMessage传递 token、密码等敏感数据 - 若必须传递,确保目标源可信且使用 HTTPS
5.5 防止 XSS 与注入
- 接收到的数据若用于
innerHTML、eval,需严格转义 - 使用
textContent替代innerHTML - 对消息类型做白名单校验
5.6 防止消息伪造
event.source可被用于回复,但不应作为唯一信任依据- 使用
MessageChannel建立私有通道,避免全局message事件被其他脚本监听
六、常见问题与陷阱
6.1 消息发送过早
iframe 或弹窗尚未加载完成时发送消息,接收方还未注册监听。
解决 :等待 load 事件或接收方主动发送 READY 消息后再通信。
js
iframe.addEventListener('load', () => {
iframe.contentWindow.postMessage({ type: 'INIT' }, 'https://child.example.com')
})
6.2 循环通信导致死循环
双方互相回复且无终止条件,导致无限消息循环。
解决:设置消息类型与终止条件,避免无脑回复。
6.3 大数据传输性能问题
传输大对象时结构化克隆开销大。
解决:
- 使用
transfer转移 ArrayBuffer - 分片传输
- 考虑使用 SharedArrayBuffer(需 COOP/COEP 头支持)
6.4 结构化克隆失败
传输函数、DOM 节点、Symbol 等会抛 DataCloneError。
解决:传输前过滤不可克隆的数据,或转换为可序列化格式。
6.5 内存泄漏
长期持有 event.source 引用或未移除 message 监听,可能导致内存泄漏。
解决:
- 组件卸载时
removeEventListener - 不必要地保存
event.source时及时置 null - 使用
MessageChannel并在结束时关闭端口
js
channel.port1.close()
channel.port2.close()
6.6 targetOrigin 与接收方源不匹配
若 targetOrigin 写错(如漏掉端口、协议不符),消息静默丢弃,难以排查。
解决:确认目标源的完整格式(协议 + 域名 + 端口),并在开发时打印日志。
6.7 Worker 与主线程的差异
- Worker 中
postMessage不需要targetOrigin - Worker 中
self.postMessage与port.postMessage不同 - Worker 中
event.origin为空字符串(同源)
6.8 嵌套 iframe 的 source 判断
多层嵌套时,event.source 可能是间接的,需结合 event.origin 与业务标识判断。
七、进阶用法
7.1 封装通信库
js
class Messenger {
constructor(targetWindow, targetOrigin, trustedOrigins) {
this.target = targetWindow
this.targetOrigin = targetOrigin
this.trustedOrigins = trustedOrigins
this.handlers = new Map()
this._onMessage = this._onMessage.bind(this)
window.addEventListener('message', this._onMessage)
}
on(type, handler) {
this.handlers.set(type, handler)
}
send(type, payload) {
this.target.postMessage({ type, payload }, this.targetOrigin)
}
_onMessage(event) {
if (!this.trustedOrigins.includes(event.origin)) return
const { type, payload } = event.data || {}
const handler = this.handlers.get(type)
if (handler) handler(payload, event)
}
destroy() {
window.removeEventListener('message', this._onMessage)
this.handlers.clear()
}
}
7.2 请求-响应模式
js
let msgId = 0
const pending = new Map()
function request(type, payload) {
return new Promise((resolve) => {
const id = ++msgId
pending.set(id, resolve)
targetWindow.postMessage({ id, type, payload }, targetOrigin)
})
}
window.addEventListener('message', (event) => {
if (event.origin !== targetOrigin) return
const { id, result } = event.data
if (pending.has(id)) {
pending.get(id)(result)
pending.delete(id)
}
})
7.3 使用 Comlink 简化通信
Comlink 是 Google 推出的库,将 postMessage 封装为类似 RPC 的调用。
js
// worker.js
import * as Comlink from 'comlink'
const api = {
add(a, b) { return a + b }
}
Comlink.expose(api)
// main.js
import * as Comlink from 'comlink'
const worker = new Worker('worker.js')
const api = Comlink.wrap(worker)
const result = await api.add(1, 2) // 像调用本地函数
7.4 广播通信(BroadcastChannel)
同源的不同标签页、iframe、Worker 之间广播消息。
js
const channel = new BroadcastChannel('my-channel')
channel.postMessage({ type: 'UPDATE', data: 1 })
channel.onmessage = (event) => {
console.log('收到广播:', event.data)
}
BroadcastChannel与postMessage不同,它仅限同源,但无需持有目标引用。
八、与其他通信方案对比
| 方案 | 跨源 | 跨窗口 | 跨线程 | 实时性 | 复杂度 |
|---|---|---|---|---|---|
| postMessage | ✅ | ✅ | ✅ | 异步 | 中 |
| BroadcastChannel | ❌(同源) | ✅ | ✅ | 异步 | 低 |
| MessageChannel | ✅ | ✅ | ✅ | 异步 | 中 |
| WebSocket | ✅ | ✅ | ✅ | 实时 | 高 |
| SharedWorker | ❌(同源) | ✅ | ✅ | 异步 | 中 |
| localStorage 事件 | ❌(同源) | ✅ | ❌ | 异步 | 低 |
| document.domain | ❌(已废弃) | ✅ | ❌ | - | 低 |
九、总结
postMessage 是前端跨上下文通信的基石,掌握它需要理解三个层面:
- 用法:API 签名、事件对象、常见场景(iframe、Worker、弹窗、Service Worker)
- 原理:结构化克隆、转移所有权、异步事件队列、源校验机制
- 安全 :始终校验
origin、精确指定targetOrigin、校验消息结构、避免敏感数据
核心实践原则:
发送时明确目标源,接收时校验来源,传输时注意克隆限制,结束时清理监听与端口。
配合 MessageChannel、BroadcastChannel、Comlink 等工具,可以构建出安全、高效、可维护的跨上下文通信体系。