2025-04-18
前端技术
0

目录

利用postMessage进行跨窗口/跨域通信
基本用法
1. 发送消息(发送方)
2. 接收消息(接收方)
关键参数
与 HTTP 的 POST 方法区别
安全注意事项
示例场景
跨域 iframe 通信
原生 JavaScript 跨域通信插件设计
插件实现代码
使用示例
1. 基本使用
2. Promise 风格使用
设计特点
高级用法
1. 多窗口广播
2. 自定义消息处理器
3. 超时处理

利用postMessage进行跨窗口/跨域通信

postMessage 是 Web API 中用于跨文档通信(Cross-document Messaging)的方法,但需要注意的是,它并不是 HTTP 协议的一部分,而是浏览器提供的 JavaScript API。它通常用于以下场景:

  1. 跨窗口/跨域通信:在不同窗口、iframe 或不同域的页面之间安全地传递数据。
  2. Web Workers:主线程与 Web Worker 线程之间的通信。
  3. Service Workers:与 Service Worker 交互。

基本用法

1. 发送消息(发送方)

javascript
// 发送消息到另一个窗口或 iframe const targetWindow = document.getElementById('iframe').contentWindow; targetWindow.postMessage('Hello from parent!', 'https://example.com'); // 发送到 Web Worker const worker = new Worker('worker.js'); worker.postMessage({ data: 'some data' });

2. 接收消息(接收方)

javascript
// 监听 message 事件 window.addEventListener('message', (event) => { // 安全检查:验证消息来源 if (event.origin !== 'https://example.com') return; console.log('Received message:', event.data); }); // Web Worker 中接收消息 self.onmessage = (event) => { console.log('Worker received:', event.data); };

关键参数

  • data: 要传递的数据(可以是字符串、对象等)。
  • targetOrigin: 指定接收方的源('https://example.com''*' 表示不限制)。这是安全机制,防止数据泄露到不可信的域名。
  • event 对象的属性
    • event.data: 接收到的数据。
    • event.origin: 发送方的源(用于验证身份)。
    • event.source: 发送方的窗口引用(可用于回复消息)。

与 HTTP 的 POST 方法区别

特性postMessage (JavaScript API)HTTP POST 方法
用途跨文档/跨线程通信客户端向服务器提交数据
协议层浏览器 API,非 HTTP 协议HTTP 协议的一部分
数据传输方向窗口 ↔ 窗口、主线程 ↔ Worker客户端 → 服务器
安全性依赖 origin 验证依赖 HTTPS、CORS 等

安全注意事项

  1. 始终验证 event.origin:防止恶意网站伪造消息。
  2. 避免使用 * 作为 targetOrigin:除非明确需要广播到所有域名。
  3. 敏感数据需加密:即使同源,也应避免明文传输敏感信息。

示例场景

跨域 iframe 通信

html
<!-- 父页面 (https://parent.com) --> <iframe id="child" src="https://child.com"></iframe> <script> const iframe = document.getElementById('child'); iframe.contentWindow.postMessage('Hello child!', 'https://child.com'); </script> <!-- 子页面 (https://child.com) --> <script> window.addEventListener('message', (event) => { if (event.origin !== 'https://parent.com') return; console.log('Child received:', event.data); event.source.postMessage('Hi parent!', event.origin); // 回复 }); </script>

原生 JavaScript 跨域通信插件设计

下面将设计一个基于原生 JavaScript 的 postMessage 通信插件,它具有以下特点:

  • 完全原生实现,不依赖任何框架
  • 使用简单,API 友好
  • 良好的扩展性
  • 内置安全机制
  • 支持双向通信

插件实现代码

javascript
/** * CrossWindowMessenger - 基于 postMessage 的跨窗口/跨域通信插件 * @param {Object} options 配置选项 */ function CrossWindowMessenger(options = {}) { // 默认配置 const defaults = { targetOrigin: window.location.origin, // 默认只允许同源通信 verifyOrigin: true, // 是否验证消息来源 namespace: 'default', // 命名空间,用于区分不同通信频道 debug: false // 调试模式 }; // 合并配置 this.settings = { ...defaults, ...options }; this.messageHandlers = {}; this.targetWindows = new Set(); // 初始化消息监听 this._initListener(); } /** * 初始化消息监听 */ CrossWindowMessenger.prototype._initListener = function() { window.addEventListener('message', (event) => { // 安全验证 if (this.settings.verifyOrigin && event.origin !== this.settings.targetOrigin) { this._log(`Message from unauthorized origin: ${event.origin}`); return; } const { data } = event; // 检查是否是本插件的消息 if (!data || data.namespace !== this.settings.namespace) { return; } this._log(`Received message:`, data); // 处理消息 if (data.type === 'request' && this.messageHandlers[data.messageId]) { // 处理请求并返回响应 const handler = this.messageHandlers[data.messageId]; const response = handler(data.payload); // 发送响应 event.source.postMessage({ namespace: this.settings.namespace, type: 'response', messageId: data.messageId, payload: response }, event.origin); } else if (data.type === 'response' && this.messageHandlers[data.messageId]) { // 处理响应 const handler = this.messageHandlers[data.messageId]; handler(data.payload); // 清理一次性处理器 delete this.messageHandlers[data.messageId]; } else if (data.type === 'event') { // 处理事件 if (this.eventHandlers && this.eventHandlers[data.eventName]) { this.eventHandlers[data.eventName].forEach(handler => { handler(data.payload); }); } } }); }; /** * 添加目标窗口 * @param {Window} targetWindow 目标窗口对象 */ CrossWindowMessenger.prototype.addTargetWindow = function(targetWindow) { this.targetWindows.add(targetWindow); return this; }; /** * 移除目标窗口 * @param {Window} targetWindow 目标窗口对象 */ CrossWindowMessenger.prototype.removeTargetWindow = function(targetWindow) { this.targetWindows.delete(targetWindow); return this; }; /** * 发送消息到所有目标窗口 * @param {String} type 消息类型 * @param {*} payload 消息内容 */ CrossWindowMessenger.prototype._sendToAll = function(type, payload) { this.targetWindows.forEach(target => { target.postMessage({ namespace: this.settings.namespace, type: type, payload: payload }, this.settings.targetOrigin); }); }; /** * 发送请求并等待响应 * @param {*} payload 请求内容 * @param {Function} callback 回调函数 * @returns {Promise} 返回Promise */ CrossWindowMessenger.prototype.request = function(payload, callback) { return new Promise((resolve) => { const messageId = this._generateId(); // 注册一次性处理器 this.messageHandlers[messageId] = (response) => { if (callback) callback(response); resolve(response); }; // 发送请求 this._sendToAll('request', { messageId: messageId, payload: payload }); }); }; /** * 发送事件到所有目标窗口 * @param {String} eventName 事件名称 * @param {*} payload 事件内容 */ CrossWindowMessenger.prototype.emit = function(eventName, payload) { this._sendToAll('event', { eventName: eventName, payload: payload }); }; /** * 监听事件 * @param {String} eventName 事件名称 * @param {Function} handler 处理函数 */ CrossWindowMessenger.prototype.on = function(eventName, handler) { if (!this.eventHandlers) { this.eventHandlers = {}; } if (!this.eventHandlers[eventName]) { this.eventHandlers[eventName] = []; } this.eventHandlers[eventName].push(handler); return this; }; /** * 取消监听事件 * @param {String} eventName 事件名称 * @param {Function} handler 处理函数 */ CrossWindowMessenger.prototype.off = function(eventName, handler) { if (!this.eventHandlers || !this.eventHandlers[eventName]) { return this; } if (handler) { const index = this.eventHandlers[eventName].indexOf(handler); if (index !== -1) { this.eventHandlers[eventName].splice(index, 1); } } else { delete this.eventHandlers[eventName]; } return this; }; /** * 生成唯一ID */ CrossWindowMessenger.prototype._generateId = function() { return Date.now().toString(36) + Math.random().toString(36).substr(2); }; /** * 调试日志 */ CrossWindowMessenger.prototype._log = function(...args) { if (this.settings.debug) { console.log('[CrossWindowMessenger]', ...args); } }; // 导出插件 if (typeof module !== 'undefined' && module.exports) { module.exports = CrossWindowMessenger; } else if (typeof define === 'function' && define.amd) { define([], function() { return CrossWindowMessenger; }); } else { window.CrossWindowMessenger = CrossWindowMessenger; }

使用示例

1. 基本使用

html
<!-- 父页面 --> <iframe id="childFrame" src="https://child-domain.com"></iframe> <script> const messenger = new CrossWindowMessenger({ targetOrigin: 'https://child-domain.com', namespace: 'myApp', debug: true }); // 添加目标窗口 const iframe = document.getElementById('childFrame'); messenger.addTargetWindow(iframe.contentWindow); // 发送请求并等待响应 messenger.request({ action: 'getUserInfo' }, (response) => { console.log('Received response:', response); }); // 发送事件 messenger.emit('userLoggedIn', { userId: 123 }); // 监听事件 messenger.on('dataUpdated', (data) => { console.log('Data updated:', data); }); </script> <!-- 子页面 (https://child-domain.com) --> <script> const childMessenger = new CrossWindowMessenger({ targetOrigin: 'https://parent-domain.com', namespace: 'myApp' }); // 添加父窗口为目标 childMessenger.addTargetWindow(window.parent); // 监听请求 childMessenger.on('request', (payload, respond) => { if (payload.action === 'getUserInfo') { respond({ name: 'John Doe', age: 30 }); } }); // 发送事件到父窗口 childMessenger.emit('dataUpdated', { newData: 'some value' }); </script>

2. Promise 风格使用

javascript
// 使用Promise messenger.request({ action: 'getData' }) .then(response => { console.log('Got response:', response); }) .catch(error => { console.error('Request failed:', error); });

设计特点

  1. 安全性

    • 强制验证消息来源 (targetOrigin)
    • 支持命名空间隔离
    • 内置消息验证机制
  2. 易用性

    • 简单的API设计 (request, emit, on, off)
    • 支持回调函数和Promise两种风格
    • 清晰的命名空间管理
  3. 扩展性

    • 支持多窗口通信
    • 可添加自定义消息处理器
    • 事件驱动架构
  4. 兼容性

    • 纯JavaScript实现,无依赖
    • 支持各种现代浏览器
    • 可轻松集成到任何项目中

高级用法

1. 多窗口广播

javascript
// 向多个iframe广播消息 const iframes = document.querySelectorAll('iframe'); const messenger = new CrossWindowMessenger({ targetOrigin: 'https://target-domain.com' }); iframes.forEach(iframe => { messenger.addTargetWindow(iframe.contentWindow); }); // 向所有iframe发送消息 messenger.emit('refreshData', { force: true });

2. 自定义消息处理器

javascript
// 添加自定义消息处理器 messenger.on('customEvent', (data) => { console.log('Custom event received:', data); // 处理自定义逻辑 }); // 发送自定义消息 messenger.emit('customEvent', { customData: 'value' });

3. 超时处理

javascript
// 添加超时功能 function requestWithTimeout(payload, timeout = 5000) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error('Request timeout')); }, timeout); messenger.request(payload) .then(response => { clearTimeout(timer); resolve(response); }) .catch(reject); }); }

这个插件设计提供了完整的跨窗口/跨域通信解决方案,同时保持了简单性和安全性。你可以根据具体需求进一步扩展或修改它。

本文作者:wucc

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-SA 许可协议。转载请注明出处!