2025前端人一文看懂 window.postMessage 通信

window.postMessage 通信指南

基础概念

window.postMessage() 是一种跨源通信的方法,允许来自不同源(域、协议或端口)的页面之间安全地进行通信。这种机制提供了一种受控的方式来规避同源策略的限制。

基础用法

发送消息

javascript 复制代码
targetWindow.postMessage(message, targetOrigin, [transfer]);

参数说明:

  • targetWindow:接收消息的窗口对象,如 iframe 的 contentWindow 属性、window.open 返回的窗口对象或命名的窗口/框架
  • message:要发送的数据,会被结构化克隆算法序列化
  • targetOrigin:指定接收消息的窗口的源,可以是具体的 URL 或 "*"(表示任意源)
  • transfer:(可选)是一组可转移对象,这些对象的所有权将被转移给接收方

接收消息

javascript 复制代码
window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
  // 验证发送方的源
  if (event.origin !== "https://trusted-domain.com") return;
  
  // 处理消息
  console.log("收到消息:", event.data);
  
  // 可以回复消息
  event.source.postMessage("收到你的消息", event.origin);
}

event 对象的主要属性:

  • data:从其他窗口发送过来的数据
  • origin:发送消息的窗口的源
  • source:发送消息的窗口对象的引用
  • ports:MessageChannel 的端口对象数组

使用场景

  1. iframe 通信:父页面与嵌入的 iframe 之间的数据交换
  2. 跨域窗口通信:通过 window.open 打开的不同域的窗口间通信
  3. Web Worker 通信:与 Web Worker 或 Service Worker 进行通信
  4. 第三方集成:与嵌入的第三方小部件或应用进行安全通信
  5. 单页应用路由:在复杂的单页应用中,不同路由间的状态同步
  6. 微前端架构:在微前端架构中,不同子应用间的数据交换和状态同步

安全注意事项

  1. 始终验证消息来源

    javascript 复制代码
    if (event.origin !== "https://trusted-domain.com") return;
  2. 避免使用 "*" 作为 targetOrigin: 尽量指定确切的目标源,而不是使用通配符 "*",以防止信息泄露给恶意网站。

  3. 验证消息内容: 不要假设收到的消息格式是正确的,始终进行验证和类型检查。

    javascript 复制代码
    if (typeof event.data !== "object" || !event.data.type) return;
  4. 避免执行来自消息的代码: 永远不要直接执行从消息中接收到的代码,如 eval(event.data)。

  5. 限制消息频率: 实现节流或防抖机制,防止消息风暴导致性能问题。

  6. 处理错误: 使用 try-catch 块处理消息处理过程中可能出现的错误。

性能考虑

  1. 消息大小:避免传输大量数据,这可能导致性能问题。
  2. 消息频率:控制消息发送的频率,避免过多的通信开销。
  3. 结构化克隆限制:了解结构化克隆算法的限制,如不能克隆函数、DOM 节点等。

浏览器兼容性

window.postMessage 在所有现代浏览器中都得到良好支持,包括 Chrome、Firefox、Safari、Edge 和 IE11。caniuse查看兼容性

调试技巧

  1. 使用浏览器开发者工具监控 message 事件
  2. 添加详细的日志记录
  3. 实现消息的确认机制
  4. 使用唯一标识符跟踪消息

最佳实践

  1. 消息格式标准化

    javascript 复制代码
    {
      type: "ACTION_TYPE",
      payload: {}, // 实际数据
      id: "unique-id", // 用于跟踪
      timestamp: Date.now()
    }
  2. 实现请求-响应模式

    javascript 复制代码
    // 发送方
    const messageId = generateUniqueId();
    const responsePromise = createResponsePromise(messageId);
    iframe.contentWindow.postMessage({
      type: "REQUEST_DATA",
      id: messageId
    }, "https://trusted-domain.com");
    
    responsePromise.then(response => {
      console.log("收到响应:", response);
    });
    
    // 接收方
    window.addEventListener("message", event => {
      if (event.origin !== "https://parent-domain.com") return;
      if (event.data.type === "REQUEST_DATA") {
        event.source.postMessage({
          type: "RESPONSE_DATA",
          id: event.data.id,
          payload: { result: "some data" }
        }, event.origin);
      }
    });
  3. 错误处理: 在消息协议中包含错误处理机制。

  4. 版本控制: 在消息中包含版本信息,以便处理 API 变更。

案例效果

vue3-project\public\child.html 下新建一个html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>PostMessage 子页面</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        padding: 20px;
        background-color: #f0f8ff;
        margin: 0;
      }
      .child-container {
        padding: 15px;
        border: 2px solid #4682b4;
        border-radius: 8px;
      }
      h2 {
        color: #4682b4;
        margin-top: 0;
      }
      .input-group {
        display: flex;
        align-items: center;
        gap: 10px;
        margin-bottom: 15px;
      }
      input {
        padding: 8px;
        border: 1px solid #ccc;
        border-radius: 4px;
        flex-grow: 1;
      }
      button {
        background-color: #4682b4;
        color: white;
        border: none;
        padding: 8px 16px;
        border-radius: 4px;
        cursor: pointer;
        transition: background-color 0.3s;
      }
      button:hover {
        background-color: #3a6d99;
      }
      .message-display {
        background-color: white;
        padding: 10px;
        border-radius: 4px;
        border-left: 4px solid #4682b4;
        margin-top: 15px;
      }
      .message-content {
        font-weight: bold;
        word-break: break-word;
      }
    </style>
  </head>
  <body>
    <div class="child-container">
      <h2>PostMessage 子页面</h2>

      <div class="input-group">
        <label for="message-input">发送消息:</label>
        <input
          id="message-input"
          type="text"
          placeholder="输入要发送的消息"
          value="你好,父页面!"
        />
        <button id="send-button">发送到父页面</button>
      </div>

      <div id="message-display" class="message-display" style="display: none">
        <h3>收到来自父页面的消息:</h3>
        <div id="message-content" class="message-content"></div>
      </div>
    </div>

    <script>
      // 页面元素
      const messageInput = document.getElementById('message-input')
      const sendButton = document.getElementById('send-button')
      const messageDisplay = document.getElementById('message-display')
      const messageContent = document.getElementById('message-content')

      /**
       * 发送消息到父窗口
       */
      function sendMessageToParent() {
        try {
          // 发送消息到父窗口
          window.parent.postMessage(
            {
              type: 'CHILD_MESSAGE',
              payload: messageInput.value,
              timestamp: Date.now(),
            },
            '*'
          ) // 在生产环境中应该指定具体的目标源
        } catch (error) {
          console.error('发送消息失败:', error)
        }
      }

      /**
       * 接收来自父窗口的消息
       */
      function handleMessage(event) {
        // 在实际应用中应该验证消息来源
        // if (event.origin !== 'https://trusted-domain.com') return;

        try {
          // 处理接收到的消息
          if (event.data && event.data.type === 'PARENT_MESSAGE') {
            messageContent.textContent = event.data.payload
            messageDisplay.style.display = 'block'
          }
        } catch (error) {
          console.error('处理消息失败:', error)
        }
      }

      // 添加事件监听器
      sendButton.addEventListener('click', sendMessageToParent)
      window.addEventListener('message', handleMessage)

      // 页面加载完成后通知父窗口
      window.addEventListener('load', function () {
        try {
          window.parent.postMessage(
            {
              type: 'CHILD_LOADED',
              payload: '子页面已加载完成',
              timestamp: Date.now(),
            },
            '*'
          )
        } catch (error) {
          console.error('通知父窗口失败:', error)
        }
      })
    </script>
  </body>
</html>

vue3-project\src\views\PostMessageView.vue

html 复制代码
<script setup>
import ParentComponent from '../components/PostMessageDemo/ParentComponent.vue'
</script>

<template>
  <div class="post-message-view">
    <h1>window.postMessage 通信演示</h1>

    <div class="description">
      <p>
        本示例演示了如何使用
        <code>window.postMessage</code> 在父页面和嵌入的iframe之间进行安全通信。
        您可以在下方的输入框中输入消息并发送,然后观察两个页面之间的通信过程。
      </p>
    </div>

    <ParentComponent />

    <div class="documentation-link">
      <p>
        查看
        <a href="#" @click.prevent="downloadDoc">完整文档</a>
        了解更多关于window.postMessage的用法和最佳实践。
      </p>
    </div>
  </div>
</template>

<script>
// 导出组件以允许使用选项API添加方法
export default {
  methods: {
    // 下载文档方法
    downloadDoc() {
      // 在实际应用中,这里可以链接到文档或触发文档下载
      window.open('/docs/postMessage.md', '_blank')
    },
  },
}
</script>

<style scoped>
.post-message-view {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

h1 {
  color: #42b883;
  text-align: center;
  margin-bottom: 30px;
}

.description {
  background-color: #f8f8f8;
  padding: 15px;
  border-radius: 8px;
  margin-bottom: 30px;
  border-left: 4px solid #42b883;
}

code {
  background-color: #e8e8e8;
  padding: 2px 5px;
  border-radius: 3px;
  font-family: monospace;
}

.documentation-link {
  margin-top: 30px;
  text-align: center;
}

.documentation-link a {
  color: #42b883;
  text-decoration: none;
  font-weight: bold;
}

.documentation-link a:hover {
  text-decoration: underline;
}
</style>

vue3-project\src\components\PostMessageDemo\ParentComponent.vue

html 复制代码
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

// 通信状态
const messageReceived = ref(null)
const messageToSend = ref('你好,子页面!')
const iframeLoaded = ref(false)

// iframe引用
const iframeRef = ref(null)

/**
 * 发送消息到iframe
 */
const sendMessage = () => {
  if (!iframeRef.value || !iframeLoaded.value) return

  try {
    // 发送消息到iframe
    iframeRef.value.contentWindow.postMessage(
      {
        type: 'PARENT_MESSAGE',
        payload: messageToSend.value,
        timestamp: Date.now(),
      },
      '*'
    ) // 在生产环境中应该指定具体的目标源
  } catch (error) {
    console.error('发送消息失败:', error)
  }
}

/**
 * 接收来自iframe的消息
 */
const handleMessage = (event) => {
  // 在实际应用中应该验证消息来源
  // if (event.origin !== 'https://trusted-domain.com') return;

  try {
    // 处理接收到的消息
    if (event.data && event.data.type === 'CHILD_MESSAGE') {
      messageReceived.value = event.data.payload
    }
  } catch (error) {
    console.error('处理消息失败:', error)
  }
}

/**
 * 处理iframe加载完成事件
 */
const handleIframeLoad = () => {
  iframeLoaded.value = true
}

// 组件挂载时添加消息监听器
onMounted(() => {
  window.addEventListener('message', handleMessage)
})

// 组件卸载时移除消息监听器
onUnmounted(() => {
  window.removeEventListener('message', handleMessage)
})
</script>

<template>
  <div class="parent-container">
    <h2>PostMessage 父组件示例</h2>

    <div class="control-panel">
      <div class="input-group">
        <label for="message-input">发送消息:</label>
        <input
          id="message-input"
          v-model="messageToSend"
          type="text"
          placeholder="输入要发送的消息"
        />
        <button @click="sendMessage" :disabled="!iframeLoaded">发送到iframe</button>
      </div>

      <div class="message-display" v-if="messageReceived">
        <h3>收到来自iframe的消息:</h3>
        <div class="message-content">{{ messageReceived }}</div>
      </div>
    </div>

    <div class="iframe-container">
      <iframe
        ref="iframeRef"
        src="/child.html"
        @load="handleIframeLoad"
        width="100%"
        height="300"
      ></iframe>
    </div>
  </div>
</template>

<style scoped>
.parent-container {
  padding: 20px;
  border: 2px solid #42b883;
  border-radius: 8px;
  margin: 20px 0;
}

.control-panel {
  margin-bottom: 20px;
}

.input-group {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 15px;
}

input {
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  flex-grow: 1;
}

button {
  background-color: #42b883;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}

button:hover {
  background-color: #33a06f;
}

button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

.message-display {
  background-color: #f8f8f8;
  padding: 10px;
  border-radius: 4px;
  border-left: 4px solid #42b883;
}

.message-content {
  font-weight: bold;
  word-break: break-word;
}

.iframe-container {
  border: 1px solid #ddd;
  border-radius: 4px;
  overflow: hidden;
}

iframe {
  border: none;
}
</style>

结尾

看懂上面的解释和说明结合vue3小案例,是不是瞬间完全明白 postMessage 怎么玩的了;以及需要注意些什么!!!

相关推荐
海天胜景3 小时前
vue3 el-table动态表头
javascript·vue.js·elementui
G_whang3 小时前
jenkins自动化部署前端vue+docker项目
前端·自动化·jenkins
凌辰揽月5 小时前
AJAX 学习
java·前端·javascript·学习·ajax·okhttp
然我6 小时前
防抖与节流:如何让频繁触发的函数 “慢下来”?
前端·javascript·html
鱼樱前端6 小时前
2025前端人一文看懂 Broadcast Channel API 通信指南
前端·vue.js
烛阴7 小时前
非空断言完全指南:解锁TypeScript/JavaScript的安全导航黑科技
前端·javascript
快乐点吧7 小时前
【前端】异步任务风控验证与轮询机制技术方案(通用笔记版)
前端·笔记
pe7er8 小时前
nuxtjs+git submodule的微前端有没有搞头
前端·设计模式·前端框架
七月的冰红茶8 小时前
【threejs】第一人称视角之八叉树碰撞检测
前端·threejs