前端 postMessage 使用场景

本文档系统讲解 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)

对于 ArrayBufferMessagePortImageBitmap 等,可通过 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 与注入

  • 接收到的数据若用于 innerHTMLeval,需严格转义
  • 使用 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.postMessageport.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)
  }
})

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)
}

BroadcastChannelpostMessage 不同,它仅限同源,但无需持有目标引用。


八、与其他通信方案对比

方案 跨源 跨窗口 跨线程 实时性 复杂度
postMessage 异步
BroadcastChannel ❌(同源) 异步
MessageChannel 异步
WebSocket 实时
SharedWorker ❌(同源) 异步
localStorage 事件 ❌(同源) 异步
document.domain ❌(已废弃) -

九、总结

postMessage 是前端跨上下文通信的基石,掌握它需要理解三个层面:

  1. 用法:API 签名、事件对象、常见场景(iframe、Worker、弹窗、Service Worker)
  2. 原理:结构化克隆、转移所有权、异步事件队列、源校验机制
  3. 安全 :始终校验 origin、精确指定 targetOrigin、校验消息结构、避免敏感数据

核心实践原则:

发送时明确目标源,接收时校验来源,传输时注意克隆限制,结束时清理监听与端口。

配合 MessageChannelBroadcastChannelComlink 等工具,可以构建出安全、高效、可维护的跨上下文通信体系。

相关推荐
zhanghaha13141 小时前
HTML系列教程:15_标签 全称 + 简写 超详细讲解(新手专用)
前端·html
zhanghaha13142 小时前
HTML系列教程:14_HTML 表单与输入框 <form>、<input> 零基础详解
java·前端·javascript
IMPYLH2 小时前
HTML 的 <samp> 元素
前端·网络·html
IMPYLH2 小时前
HTML 的 <script> 元素
前端·html
weixin_493503676 小时前
Vue3 前端生成 PDF:会员证书与活动签到表的三种打印方案与踩坑记录
前端·pdf·状态模式
李少兄8 小时前
JavaScript 数据类型完全指南
开发语言·javascript·ecmascript
Databuff8 小时前
AI SSH工具,集齐SSH、SFTP、知识库RAG、AI助手
网络·人工智能·ssh
Jeremy_WW8 小时前
QSFP/QSFP-DD/OSFP 通用管理接口规范(CMIS)解读:13 Page 9Fh
网络·网络协议·信息与通信·光模块·cmis
尾善爱看海9 小时前
前端算法与手写题集
前端·算法