技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

跨标签页通信的 8 种方式

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/38-跨标签页通信的8种方式
本文目录12 个章节
  1. 一、BroadcastChannel
  2. 二、Service Worker
  3. 三、localStorage 和 storage 事件
  4. 四、window.open() 与 postMessage()
  5. 五、SharedWorker
  6. 六、IndexedDB
  7. 七、Cookie 轮询
  8. 八、WebSocket
  9. 九、如何选择
  10. 十、安全和可靠性
  11. 总结
  12. 参考资料

跨标签页通信的 8 种方式

Category(分类): Browser Status: 已更新

不同标签页通常拥有独立的 JavaScript 全局对象和执行环境,不能直接读取彼此的普通内存。跨标签页通信要么使用浏览器提供的同源通道,要么通过服务器中转。

本文保留原文列出的 8 种方式,并补充各自的边界:

  1. BroadcastChannel
  2. Service Worker
  3. localStoragestorage 事件
  4. window.open() + window.postMessage()
  5. SharedWorker
  6. IndexedDB
  7. Cookie 轮询
  8. WebSocket

另外,MessageChannelSharedArrayBuffer(需要跨源隔离)和服务器推送也可以作为特定场景的补充,但不应把它们和上述 API 的语义混为一谈。

一、BroadcastChannel

Broadcast Channel API 可以在同源的窗口、标签页、iframe、Dedicated Worker、SharedWorker 等浏览上下文之间广播结构化数据。发送者自己的 BroadcastChannel 对象不会收到自己发送的消息,其他已连接的同源对象会收到。

BroadcastChannel 广播示意

发送方

const channel = new BroadcastChannel('app-events')

channel.postMessage({
  type: 'auth-invalidated',
  sessionVersion: 12
})

接收方

const channel = new BroadcastChannel('app-events')

channel.addEventListener('message', event => {
  if (event.data?.type === 'auth-invalidated') {
    window.location.reload()
  }
})

window.addEventListener('pagehide', () => {
  channel.close()
})

消息使用结构化克隆算法传递,可以发送对象、数组和部分二进制数据,但不能发送函数、DOM 节点等不可克隆值。BroadcastChannel 不提供消息持久化、确认、重试和历史补发;新打开的标签页不会收到过去的消息。

适合:登录失效广播、主题切换、草稿刷新、多个页面的轻量状态通知。 不适合:必须可靠投递的订单、离线队列和需要历史恢复的消息。

二、Service Worker

Service Worker 是受 origin 和 scope 限制的事件驱动 Worker,可以拦截受控页面的请求,也可以在页面之间转发消息。它适合在多个页面之间维护网络代理、缓存、离线状态和统一消息入口。

Service Worker 注册成功后还要安装、激活并控制页面。第一次打开页面时 navigator.serviceWorker.controller 可能为 null,不能直接假设当前页面已经受控。向 Worker 发消息时,可以等待 navigator.serviceWorker.ready 并使用活动 Worker。

页面代码

async function initServiceWorker() {
  await navigator.serviceWorker.register('/sw.js')
  const registration = await navigator.serviceWorker.ready

  navigator.serviceWorker.addEventListener('message', event => {
    console.log('收到 Service Worker 消息', event.data)
  })

  registration.active?.postMessage({
    type: 'broadcast',
    payload: '来自页面'
  })
}

initServiceWorker()

sw.js

self.addEventListener('message', event => {
  event.waitUntil((async () => {
    const clients = await self.clients.matchAll({
      type: 'window',
      includeUncontrolled: true
    })

    for (const client of clients) {
      client.postMessage({
        type: 'from-service-worker',
        payload: event.data
      })
    }
  })())
})

clients.matchAll() 返回的是当前作用域内可见的客户端;使用 includeUncontrolled 时要意识到这些页面可能还没有被当前 Worker 控制。真实应用还要处理 Worker 更新、版本号、异常和消息来源。

Service Worker 并不是永远运行的后台进程,浏览器可以随时终止空闲 Worker,消息处理要在事件生命周期内完成。它也不能直接共享页面内存,数据传递通过消息和结构化克隆完成。

三、localStoragestorage 事件

同源页面可以共享 localStorage。当一个文档修改 localStorage 后,其他同源文档可以收到 storage 事件;执行修改的那个文档不会收到自己的事件。

// 发送方
localStorage.setItem('app:event', JSON.stringify({
  type: 'theme-changed',
  theme: 'dark',
  at: Date.now()
}))
// 其他标签页
window.addEventListener('storage', event => {
  if (event.storageArea !== localStorage) return
  if (event.key !== 'app:event' || !event.newValue) return

  const message = JSON.parse(event.newValue)
  if (message.type === 'theme-changed') {
    document.documentElement.dataset.theme = message.theme
  }
})

注意事项:

  • 事件通常只发送给其他文档,不会回到当前写入文档;
  • 写入相同值时可能没有实际存储变化,也就不会产生预期通知;
  • 所有值都是字符串,需要自行序列化和校验;
  • storage 事件是通知机制,不提供可靠队列、确认和顺序协议;
  • 不要把高敏感令牌放在 localStorage,XSS 可以读取它;
  • sessionStorage 的事件范围与顶级浏览上下文有关,不适合当作普遍的跨标签页通道。

如果只是同步“当前状态”,可以把状态写入 localStorage,接收方收到事件后重新读取完整状态,而不是把整个业务消息队列塞进一个键。

四、window.open()postMessage()

当一个窗口通过 window.open() 打开另一个窗口时,打开方可能得到一个 WindowProxy 引用,并通过 postMessage() 定向发送消息。这个方案可用于同源和跨源页面,但跨源页面只能访问受限制的 Window API,不能直接访问对方 DOM。

发送方

const child = window.open('/child.html', 'child-window')
const childOrigin = window.location.origin

child?.postMessage({
  type: 'hello',
  payload: '来自父窗口'
}, childOrigin)

弹窗可能被浏览器拦截,因此 window.open() 可能返回 null。跨源场景应使用接收页面的准确 targetOrigin,不要长期使用 '*' 发送敏感数据。

接收方

window.addEventListener('message', event => {
  const expectedOrigin = window.location.origin

  if (event.origin !== expectedOrigin) return
  if (event.data?.type !== 'hello') return

  console.log('收到消息', event.data.payload)
})

生产代码还应校验 event.source、消息类型、数据结构和请求时机。event.origin 是发送时的 origin,不应只根据当前地址栏判断信任关系。COOP、跨源隔离、窗口关闭和浏览器隐私策略也可能影响 window.opener 和窗口引用。

如果两个页面需要互相建立多个独立通道,可以用 MessageChannel 创建两个 MessagePort,再通过 postMessage() 把其中一个端口转移给对方。

五、SharedWorker

SharedWorker 是可被多个同源窗口、iframe 或 Worker 连接的共享 JavaScript 执行环境。页面通过 worker.port 与它通信;同一个 origin、脚本 URL 和名称组合通常连接到同一个 SharedWorker。

SharedWorker 兼容性和连接示意

页面代码:

const worker = new SharedWorker('/shared-worker.js')

worker.port.addEventListener('message', event => {
  console.log('收到其他页面的消息', event.data)
})
worker.port.start()

worker.port.postMessage({
  type: 'presence',
  online: true
})

shared-worker.js

const ports = new Set()

self.addEventListener('connect', event => {
  const port = event.ports[0]
  ports.add(port)
  port.start()

  port.addEventListener('message', messageEvent => {
    for (const target of ports) {
      if (target !== port) target.postMessage(messageEvent.data)
    }
  })

})

MessagePort 没有一个可依赖的通用“对端关闭”事件;生产实现可以用心跳、显式注销消息或超时清理 ports 集合。SharedWorker 的生命周期和浏览器支持情况需要确认,部分浏览器或嵌入环境可能不支持或行为不同。它不是“页面生命周期内必然永远存在的唯一线程”,空闲时也可能被回收;需要持久状态时应写入 IndexedDB 或服务端。

六、IndexedDB

IndexedDB 是异步、事务型的客户端数据库,适合保存较大量的结构化数据、离线队列和跨页面共享状态。它不是天然的实时广播通道,也没有一个普遍可用的“数据变更事件”可以通知其他标签页。

写入持久状态:

const request = indexedDB.open('app-state', 1)

request.onupgradeneeded = () => {
  request.result.createObjectStore('messages', {
    keyPath: 'id',
    autoIncrement: true
  })
}

request.onsuccess = () => {
  const db = request.result
  const transaction = db.transaction('messages', 'readwrite')
  transaction.objectStore('messages').add({
    type: 'draft-updated',
    value: 'hello',
    createdAt: Date.now()
  })
}

读取数据并通知页面:

const readRequest = indexedDB.open('app-state', 1)

readRequest.onsuccess = () => {
  const db = readRequest.result
  const transaction = db.transaction('messages', 'readonly')
  const request = transaction.objectStore('messages').getAll()

  request.onsuccess = () => {
    console.log('持久化消息', request.result)
  }
}

原文用 setInterval() 轮询 IndexedDB 实现通信,这可以作为兼容性兜底,但会带来延迟、重复消费、竞态、数据库唤醒和无效读取。更实用的组合是:IndexedDB 保存可靠状态,BroadcastChannel 或 Service Worker 发送“有更新”的即时通知;如果通知丢失,再在页面恢复时读取数据库。

多个标签页同时消费队列时,需要设计唯一消息 ID、幂等处理、事务和重试策略,不能简单地 getAll() 后让每个页面都删除同一批数据。

七、Cookie 轮询

Cookie 会在满足 Domain、Path、Secure、SameSite 等条件时自动附加到 HTTP 请求,因此不同标签页可以看到同一作用域下的非 HttpOnly Cookie。通过定时读取 document.cookie,理论上可以发现变化:

// HTTPS 页面中的低敏感状态示例
const message = encodeURIComponent(JSON.stringify({
  type: 'refresh',
  at: Date.now()
}))

document.cookie = `tab_event=${message}; Max-Age=60; Path=/; SameSite=Lax; Secure`
const timer = window.setInterval(() => {
  const item = document.cookie
    .split('; ')
    .find(value => value.startsWith('tab_event='))

  if (!item) return

  const value = decodeURIComponent(item.slice('tab_event='.length))
  console.log(JSON.parse(value))
}, 1000)

window.addEventListener('pagehide', () => clearInterval(timer))

这不是推荐的现代消息方案:

  • Cookie 容量小,写入还会增加后续 HTTP 请求大小;
  • 没有原生变更事件,轮询有延迟和电量成本;
  • 同名 Cookie 可能受 Domain、Path 和过期时间影响;
  • HttpOnly Cookie 无法由 JavaScript 读取;
  • Cookie 会自动随请求发送,不能保存任意敏感消息;
  • 多个标签页同时读写容易覆盖或重复消费。

只有在极老旧环境或极简单的低敏感标记场景,才考虑把 Cookie 作为兼容兜底。

八、WebSocket

WebSocket 建立的是浏览器与服务器之间的长连接,并不是标签页之间的直接通道。多个标签页各自连接服务器,由服务器按用户、房间或订阅关系广播消息,因此可以实现跨标签页、跨设备甚至跨客户端同步。

浏览器客户端:

const socket = new WebSocket('wss://example.com/realtime')

socket.addEventListener('open', () => {
  socket.send(JSON.stringify({
    type: 'join',
    room: 'user-42'
  }))
})

socket.addEventListener('message', event => {
  const message = JSON.parse(event.data)
  console.log('收到服务器广播', message)
})

socket.addEventListener('close', () => {
  console.log('连接关闭,可按退避策略重连')
})

服务端需要维护连接、身份认证、订阅关系、广播和重连后的状态补发。例如使用 Node.js ws 库时,逻辑可以是:

const { WebSocketServer, WebSocket } = require('ws')
const wss = new WebSocketServer({ port: 8080 })

wss.on('connection', socket => {
  socket.on('message', raw => {
    for (const client of wss.clients) {
      if (client.readyState === WebSocket.OPEN) {
        client.send(raw)
      }
    }
  })
})

生产环境应使用 wss、校验 Origin 和身份、限制消息大小、处理心跳/超时、指数退避重连、重复消息和离线补偿。WebSocket 适合实时协作和服务端主动通知,但为了只同步同一浏览器的几个标签页,引入服务器连接通常比 BroadcastChannel 更重。

九、如何选择

方式通信范围是否持久化实时性适合场景
BroadcastChannel同源浏览上下文同浏览器页面广播
Service Worker同源作用域内客户端可配合 Cache/IDB网络代理、离线和页面协调
storage 事件同源其他文档localStorage 持久简单状态变化通知
postMessage有窗口/端口引用的页面父子窗口、跨源受控通信
SharedWorker同源连接页面Worker 自身不保证持久多页面共享一个 Worker
IndexedDB同源持久数据低到中离线数据、可靠状态和队列
Cookie 轮询Cookie 作用域内页面极旧环境的低敏感兼容方案
WebSocket经服务器到多个客户端需自行设计实时、多设备、多用户同步

十、安全和可靠性

  1. 先明确通信边界:同源、同站点、跨源还是跨设备;
  2. postMessage 必须校验 event.originevent.source、消息类型和数据结构;
  3. BroadcastChannel、Storage、IndexedDB 和 Cookie 都不能自动证明消息可信,收到消息后仍要做业务鉴权;
  4. 不要把高敏感 Token 复制到 localStorage、Cookie 可读值或广播消息中;
  5. 需要可靠消息时设计 ID、幂等、确认、重试、版本和状态重建;
  6. 页面刷新、浏览器崩溃、后台冻结、隐私分区和存储清理都可能让瞬时消息丢失;
  7. 用 BroadcastChannel 发通知、用 IndexedDB 保存状态,通常比把 IndexedDB 或 Cookie 当作实时消息队列更稳妥。

总结

跨标签页通信没有一种方式适合全部场景:

  • 只需要同源实时广播,优先 BroadcastChannel
  • 需要离线、请求拦截或统一客户端管理,考虑 Service Worker;
  • 只同步简单状态,可以使用 storage 事件;
  • 有父子窗口引用时使用 postMessage 并严格校验来源;
  • 需要持久状态使用 IndexedDB,必要时配合广播通知;
  • WebSocket 适合服务器参与的实时同步;
  • Cookie 轮询应仅作为低敏感、旧环境的兼容方案。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS