2025前端人一文看懂 Broadcast Channel API 通信指南

大家好,我是鱼樱!!!

关注公众号【鱼樱AI实验室】持续分享更多前端和AI辅助前端编码新知识~~

不定时写点笔记写点生活~写点前端经验。

在当前环境下,纯前端开发者可以通过技术深化、横向扩展、切入新兴领域以及产品化思维找到突破口。

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

前端最卷的开发语言一点不为过,三天一小更,五天一大更。。。一年一个框架升级~=嗯,要的就是这样感觉!与时俱进~

Broadcast Channel API 通信指南

基础概念

Broadcast Channel API 是一种同源通信机制,允许同源的不同浏览器上下文(如窗口、标签页、iframe、worker等)之间进行通信。这种机制提供了一种简单高效的方式,让同源的不同页面能够实时交换信息。

基础用法

创建和连接频道

javascript 复制代码
// 创建或连接到名为"example-channel"的广播频道
const channel = new BroadcastChannel('example-channel');

发送消息

javascript 复制代码
// 发送消息到所有监听该频道的接收者
channel.postMessage({
  type: 'UPDATE',
  payload: { message: '这是一条广播消息' },
  timestamp: Date.now()
});

接收消息

javascript 复制代码
// 监听频道上的消息
channel.addEventListener('message', (event) => {
  console.log('收到消息:', event.data);
});

// 或者使用onmessage事件处理器
channel.onmessage = (event) => {
  console.log('收到消息:', event.data);
};

关闭频道

javascript 复制代码
// 当不再需要频道时关闭它
channel.close();

与 window.postMessage 的区别

  1. 作用范围

    • Broadcast Channel API:仅限于同源的浏览上下文之间通信
    • window.postMessage:可以跨域通信
  2. 通信模式

    • Broadcast Channel API:一对多广播模式,发送者不需要知道接收者
    • window.postMessage:一对一通信模式,需要明确指定目标窗口
  3. 使用便捷性

    • Broadcast Channel API:API更简单,不需要引用其他窗口对象
    • window.postMessage:需要获取目标窗口的引用

使用场景

  1. 多标签页应用同步

    • 用户在一个标签页中进行的操作可以自动同步到同一应用的其他标签页
    • 例如:用户在一个标签页中更改设置,其他标签页立即更新
  2. 实时通知系统

    • 在不同标签页之间传递实时通知
    • 例如:在一个标签页收到新消息时,其他标签页显示通知
  3. 共享状态管理

    • 在不同标签页之间共享应用状态
    • 例如:用户认证状态同步,一个标签页登出后其他标签页也随之登出
  4. 协作应用

    • 在多个标签页之间协同工作
    • 例如:多人编辑文档时的实时协作
  5. Service Worker 通信

    • 在页面和 Service Worker 之间进行通信
    • 例如:Service Worker 接收到推送通知后广播给所有活动页面

安全注意事项

  1. 仅限同源通信: Broadcast Channel API 仅允许同源页面之间通信,这是一种内置的安全限制。

  2. 消息验证: 尽管是同源通信,仍然建议对接收到的消息进行验证,确保其格式和内容符合预期。

  3. 敏感数据处理: 避免通过广播频道传输敏感信息,因为同源的任何页面都可以监听该频道。

性能考虑

  1. 消息大小: 虽然理论上没有严格的大小限制,但发送大量数据可能影响性能,应尽量保持消息简洁。

  2. 频道数量: 合理控制频道数量,避免创建过多不必要的频道。

  3. 关闭不用的频道 : 当不再需要某个频道时,调用 close() 方法释放资源。

浏览器兼容性

Broadcast Channel API 在现代浏览器中得到良好支持,包括 Chrome、Firefox、Edge 和 Safari。但在 IE 中不支持,需要使用 polyfill 或替代方案。

最佳实践

  1. 消息格式标准化

    javascript 复制代码
    {
      type: "ACTION_TYPE",
      payload: {}, // 实际数据
      timestamp: Date.now(),
      source: "tab-identifier" // 可选,标识发送源
    }
  2. 错误处理

    javascript 复制代码
    try {
      channel.postMessage(message);
    } catch (error) {
      console.error('发送消息失败:', error);
    }
  3. 生命周期管理: 在组件卸载或页面关闭前关闭频道:

    javascript 复制代码
    // React组件示例
    useEffect(() => {
      const channel = new BroadcastChannel('my-channel');
      
      // 设置监听器
      channel.onmessage = handleMessage;
      
      // 清理函数
      return () => {
        channel.close();
      };
    }, []);
  4. 消息去重: 对于某些应用场景,可能需要实现消息去重机制,避免重复处理相同的消息。

Broadcast Channel API 案例

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

// 购物车数据
const cartItems = ref([
  { id: 1, name: '商品A', price: 99, quantity: 1 },
  { id: 2, name: '商品B', price: 199, quantity: 1 },
])

// 广播频道实例
let broadcastChannel = null

// 当前标签页ID
const tabId = ref(`tab-${Date.now()}-${Math.floor(Math.random() * 1000)}`)

// 消息日志
const messageLog = ref([])

/**
 * 添加商品到购物车
 * @param {Object} product - 要添加的商品
 */
const addToCart = (product) => {
  // 检查商品是否已存在于购物车中
  const existingItem = cartItems.value.find((item) => item.id === product.id)

  if (existingItem) {
    // 如果商品已存在,增加数量
    existingItem.quantity += 1
  } else {
    // 如果商品不存在,添加到购物车
    cartItems.value.push({ ...product, quantity: 1 })
  }

  // 广播购物车更新消息
  broadcastCartUpdate()
}

/**
 * 从购物车中移除商品
 * @param {Number} productId - 要移除的商品ID
 */
const removeFromCart = (productId) => {
  const index = cartItems.value.findIndex((item) => item.id === productId)
  if (index !== -1) {
    cartItems.value.splice(index, 1)

    // 广播购物车更新消息
    broadcastCartUpdate()
  }
}

/**
 * 更新购物车中商品数量
 * @param {Number} productId - 商品ID
 * @param {Number} quantity - 新数量
 */
const updateQuantity = (productId, quantity) => {
  const item = cartItems.value.find((item) => item.id === productId)
  if (item) {
    item.quantity = Math.max(1, quantity) // 确保数量至少为1

    // 广播购物车更新消息
    broadcastCartUpdate()
  }
}

/**
 * 将响应式对象转换为普通对象
 * @param {Object} obj - 响应式对象
 * @returns {Object} - 普通对象
 */
const toRawObject = (obj) => {
  return JSON.parse(JSON.stringify(obj))
}

/**
 * 广播购物车更新消息
 */
const broadcastCartUpdate = () => {
  if (!broadcastChannel) return

  try {
    // 将响应式对象转换为普通对象
    const plainItems = toRawObject(cartItems.value)
    const total = calculateTotal()

    broadcastChannel.postMessage({
      type: 'CART_UPDATE',
      payload: {
        items: plainItems,
        total: total,
      },
      source: tabId.value,
      timestamp: Date.now(),
    })

    // 添加到消息日志
    addToMessageLog('发送', '购物车数据已广播到其他标签页')
  } catch (error) {
    console.error('广播购物车更新失败:', error)
    addToMessageLog('错误', `广播失败: ${error.message}`)
  }
}

/**
 * 处理接收到的消息
 * @param {MessageEvent} event - 消息事件
 */
const handleMessage = (event) => {
  const { type, payload, source, timestamp } = event.data

  // 忽略自己发送的消息
  if (source === tabId.value) return

  if (type === 'CART_UPDATE') {
    // 更新购物车数据
    cartItems.value = payload.items

    // 添加到消息日志
    addToMessageLog('接收', `从标签页 ${source} 接收到购物车更新`)
  }
}

/**
 * 添加消息到日志
 * @param {String} direction - 消息方向(发送/接收)
 * @param {String} content - 消息内容
 */
const addToMessageLog = (direction, content) => {
  messageLog.value.push({
    direction,
    content,
    time: new Date().toLocaleTimeString(),
  })

  // 限制日志条数
  if (messageLog.value.length > 10) {
    messageLog.value.shift()
  }
}

/**
 * 计算购物车总价
 * @returns {Number} 总价
 */
const calculateTotal = () => {
  return cartItems.value.reduce((total, item) => total + item.price * item.quantity, 0)
}

/**
 * 清空购物车
 */
const clearCart = () => {
  cartItems.value = []
  broadcastCartUpdate()
}

/**
 * 打开新标签页
 */
const openNewTab = () => {
  window.open(window.location.href, '_blank')
}

// 组件挂载时初始化广播频道
onMounted(() => {
  try {
    broadcastChannel = new BroadcastChannel('shopping-cart-channel')
    broadcastChannel.onmessage = handleMessage

    // 广播初始状态
    setTimeout(() => {
      broadcastCartUpdate()
    }, 500)

    addToMessageLog('系统', `标签页 ${tabId.value} 已连接到广播频道`)
  } catch (error) {
    console.error('创建广播频道失败:', error)
    addToMessageLog('错误', '创建广播频道失败,可能是浏览器不支持')
  }
})

// 组件卸载时关闭广播频道
onUnmounted(() => {
  if (broadcastChannel) {
    broadcastChannel.close()
    broadcastChannel = null
  }
})
</script>

<template>
  <div class="cart-container">
    <div class="cart-header">
      <h2>Broadcast Channel 购物车示例</h2>
      <p class="tab-id">当前标签页ID: {{ tabId }}</p>
    </div>

    <div class="cart-actions">
      <button @click="openNewTab" class="action-button">打开新标签页</button>
      <button @click="clearCart" class="action-button clear">清空购物车</button>
    </div>

    <div class="cart-content">
      <div class="cart-items">
        <h3>购物车商品</h3>

        <div v-if="cartItems.length === 0" class="empty-cart">购物车为空</div>

        <div v-else class="item-list">
          <div v-for="item in cartItems" :key="item.id" class="cart-item">
            <div class="item-info">
              <span class="item-name">{{ item.name }}</span>
              <span class="item-price">¥{{ item.price }}</span>
            </div>

            <div class="item-actions">
              <button @click="updateQuantity(item.id, item.quantity - 1)" class="quantity-btn">
                -
              </button>
              <span class="quantity">{{ item.quantity }}</span>
              <button @click="updateQuantity(item.id, item.quantity + 1)" class="quantity-btn">
                +
              </button>
              <button @click="removeFromCart(item.id)" class="remove-btn">删除</button>
            </div>
          </div>

          <div class="cart-total">总计: ¥{{ calculateTotal() }}</div>
        </div>
      </div>

      <div class="product-list">
        <h3>可购买商品</h3>

        <div class="products">
          <div class="product-item" @click="addToCart({ id: 3, name: '商品C', price: 299 })">
            <div class="product-name">商品C</div>
            <div class="product-price">¥299</div>
            <button class="add-btn">添加到购物车</button>
          </div>

          <div class="product-item" @click="addToCart({ id: 4, name: '商品D', price: 399 })">
            <div class="product-name">商品D</div>
            <div class="product-price">¥399</div>
            <button class="add-btn">添加到购物车</button>
          </div>

          <div class="product-item" @click="addToCart({ id: 5, name: '商品E', price: 499 })">
            <div class="product-name">商品E</div>
            <div class="product-price">¥499</div>
            <button class="add-btn">添加到购物车</button>
          </div>
        </div>
      </div>
    </div>

    <div class="message-log">
      <h3>通信日志</h3>

      <div class="log-entries">
        <div
          v-for="(log, index) in messageLog"
          :key="index"
          :class="[
            'log-entry',
            log.direction === '发送' ? 'sent' : log.direction === '接收' ? 'received' : 'system',
          ]"
        >
          <span class="log-time">{{ log.time }}</span>
          <span class="log-direction">[{{ log.direction }}]</span>
          <span class="log-content">{{ log.content }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.cart-container {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  background-color: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.cart-header {
  margin-bottom: 20px;
  padding-bottom: 10px;
  border-bottom: 1px solid #eee;
}

.cart-header h2 {
  color: #333;
  margin-top: 0;
}

.tab-id {
  font-size: 0.9em;
  color: #666;
  margin-top: 5px;
}

.cart-actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 20px;
}

.action-button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background-color: #4682b4;
  color: white;
  cursor: pointer;
  transition: background-color 0.3s;
}

.action-button:hover {
  background-color: #3a6d99;
}

.action-button.clear {
  background-color: #e74c3c;
}

.action-button.clear:hover {
  background-color: #c0392b;
}

.cart-content {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin-bottom: 20px;
}

.cart-items,
.product-list {
  padding: 15px;
  border: 1px solid #eee;
  border-radius: 4px;
  background-color: #f9f9f9;
}

h3 {
  margin-top: 0;
  color: #333;
  border-bottom: 1px solid #eee;
  padding-bottom: 10px;
}

.empty-cart {
  padding: 20px;
  text-align: center;
  color: #999;
}

.cart-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid #eee;
}

.item-info {
  display: flex;
  flex-direction: column;
}

.item-name {
  font-weight: bold;
}

.item-price {
  color: #e74c3c;
}

.item-actions {
  display: flex;
  align-items: center;
}

.quantity-btn {
  width: 25px;
  height: 25px;
  border: 1px solid #ddd;
  background-color: #f5f5f5;
  border-radius: 3px;
  cursor: pointer;
}

.quantity {
  margin: 0 10px;
  min-width: 20px;
  text-align: center;
}

.remove-btn {
  margin-left: 10px;
  padding: 5px 10px;
  border: none;
  background-color: #e74c3c;
  color: white;
  border-radius: 3px;
  cursor: pointer;
}

.cart-total {
  margin-top: 15px;
  padding-top: 10px;
  border-top: 1px solid #eee;
  text-align: right;
  font-weight: bold;
}

.products {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 15px;
}

.product-item {
  padding: 15px;
  border: 1px solid #eee;
  border-radius: 4px;
  background-color: white;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
}

.product-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.product-name {
  font-weight: bold;
  margin-bottom: 5px;
}

.product-price {
  color: #e74c3c;
  margin-bottom: 10px;
}

.add-btn {
  width: 100%;
  padding: 5px;
  border: none;
  background-color: #2ecc71;
  color: white;
  border-radius: 3px;
  cursor: pointer;
}

.message-log {
  margin-top: 20px;
  padding: 15px;
  border: 1px solid #eee;
  border-radius: 4px;
  background-color: #f9f9f9;
}

.log-entries {
  max-height: 200px;
  overflow-y: auto;
}

.log-entry {
  padding: 8px;
  margin-bottom: 5px;
  border-radius: 3px;
  font-size: 0.9em;
}

.log-entry.sent {
  background-color: #e8f4fd;
  border-left: 3px solid #3498db;
}

.log-entry.received {
  background-color: #f0fff0;
  border-left: 3px solid #2ecc71;
}

.log-entry.system {
  background-color: #f8f8f8;
  border-left: 3px solid #95a5a6;
}

.log-entry.error {
  background-color: #fff0f0;
  border-left: 3px solid #e74c3c;
}

.log-time {
  color: #666;
  margin-right: 5px;
}

.log-direction {
  font-weight: bold;
  margin-right: 5px;
}

.log-content {
  color: #333;
}
</style>

结尾

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

相关推荐
weixin_472339461 分钟前
Web应用性能优化之数据库查询实战指南
前端·性能优化
问道飞鱼9 分钟前
【前端知识】移动端APP原生应用与H5交互底层逻辑
前端·交互·webview·jsbridge
海天胜景5 小时前
vue3 el-table动态表头
javascript·vue.js·elementui
G_whang5 小时前
jenkins自动化部署前端vue+docker项目
前端·自动化·jenkins
凌辰揽月7 小时前
AJAX 学习
java·前端·javascript·学习·ajax·okhttp
然我9 小时前
防抖与节流:如何让频繁触发的函数 “慢下来”?
前端·javascript·html
烛阴9 小时前
非空断言完全指南:解锁TypeScript/JavaScript的安全导航黑科技
前端·javascript
鱼樱前端9 小时前
2025前端人一文看懂 window.postMessage 通信
前端·vue.js
快乐点吧10 小时前
【前端】异步任务风控验证与轮询机制技术方案(通用笔记版)
前端·笔记