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

显示模式

登录
ARCHIVE DOCUMENTBR

前端跨页面通信,你知道哪些方法

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/23-前端跨页面通信,你知道哪些方法
本文目录14 个章节
  1. 一、先区分源、上下文和通信目标
  2. 二、BroadcastChannel:同源广播的首选
  3. 三、localStorage + storage 事件:兼容性较好的轻量方案
  4. 四、postMessage:窗口、弹窗和 iframe 的跨源通信
  5. 五、MessageChannel:创建一对专用端口
  6. 六、Service Worker:以它为中转站
  7. 七、SharedWorker:多个页面共享一个 Worker
  8. 八、IndexedDB:持久状态 + 通知,而不是高频轮询
  9. 九、Cookie 和服务器推送
  10. 十、非同源页面如何通信
  11. 十一、消息协议的工程化
  12. 十二、方式对比
  13. 总结
  14. 参考资料

前端跨页面通信,你知道哪些方法

Category(分类): Browser, Web API Status: 已更新

浏览器中的每个页面都有自己的 JavaScript 全局对象和运行上下文。列表页、详情页、多个标签页、弹窗、iframe 之间有时需要同步登录状态、收藏状态、主题、草稿或上传进度,这就是跨页面(跨文档/跨上下文)通信。

原文介绍了 BroadcastChannel、Service Worker、localStorage、SharedWorker、IndexedDB、window.opener 和 WebSocket,并尝试用 iframe 作为跨域桥。下面保留这些方法,同时修正同源、生命周期、消息安全和“服务器推送”等概念。

一、先区分源、上下文和通信目标

1. 同源不等于同一个页面

同源要求协议、主机和端口都相同:

https://example.com:443/a
https://example.com:443/b

上面两个 URL 同源,但它们可以位于不同标签页、窗口或 iframe 中,不能直接共享普通 JavaScript 变量。

下面的任意一项不同,都不是同源:

http://example.com       # 协议不同
https://admin.example.com # 主机不同
https://example.com:8443  # 端口不同

“跨域”在实际讨论中可能指 CORS、跨源 postMessage、跨站 Cookie 或同源策略,不能用一个 API 解决所有问题。

2. 选择方式前先问四个问题

  1. 页面是否同源?
  2. 需要一对一、广播,还是服务器向多个页面推送?
  3. 消息是否允许丢失?是否需要离线后补发?
  4. 接收方是否可信,消息中是否包含敏感数据?

短小的“收藏状态变化”适合 BroadcastChannel;需要跨源 iframe 通信适合 postMessage;需要可靠恢复的任务状态应使用 IndexedDB 保存状态,再用消息通知刷新,而不是把 IndexedDB 当成高频消息队列。

跨页面通信的常见场景示意

二、BroadcastChannel:同源广播的首选

Broadcast Channel API 允许同源的窗口、标签页、iframe、Worker 和 Service Worker 加入同名频道。数据通过结构化克隆算法传递,不要求手动 JSON.stringify()

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

channel.addEventListener('message', event => {
  const message = event.data
  if (message?.type !== 'favorite-changed') return

  updateFavoriteUI(message.articleId, message.favorite)
})

channel.postMessage({
  type: 'favorite-changed',
  articleId: 'article-42',
  favorite: true,
  messageId: crypto.randomUUID(),
  sentAt: Date.now()
})

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

特点和限制

  • 只在同源上下文之间工作,不是任意“同域名”都可以;
  • 一个频道可以一对多广播;
  • 同一个 BroadcastChannel 对象不会收到自己发出的消息,其他已连接对象可以收到;
  • 消息不提供持久化、确认、重试和历史回放;
  • 页面被冻结、挂起或关闭时,不能保证接收消息;
  • 使用完应调用 close(),避免长期页面创建大量频道。

它很适合“状态已经保存在服务端或 IndexedDB,消息只负责提示其他页面刷新”的场景:

BroadcastChannel 广播示意

// 不把完整订单、Token 或用户隐私直接广播出去
channel.postMessage({
  type: 'order-updated',
  orderId: 'order-42',
  version: 8
})

三、localStorage + storage 事件:兼容性较好的轻量方案

当同源页面修改 localStorage 时,其他页面可以收到 storage 事件:

window.addEventListener('storage', event => {
  if (event.storageArea !== localStorage) return
  if (event.key !== 'app-event' || !event.newValue) return

  try {
    const message = JSON.parse(event.newValue)
    handleMessage(message)
  } catch {
    // 忽略格式错误的消息
  }
})

function sendMessage(message) {
  localStorage.setItem('app-event', JSON.stringify({
    ...message,
    messageId: crypto.randomUUID(),
    sentAt: Date.now()
  }))
}

必须记住的细节

  • storage 事件通常只发送给其他同源文档,不会在修改值的同一个文档中触发;
  • sessionStorage 的事件范围与标签页/iframe 关系不同,跨标签页同步通常使用 localStorage
  • 设置相同的字符串值可能不会产生新的事件,所以历史代码常添加时间戳,但更好的方式是添加唯一 messageId
  • clear()event.keyevent.newValue 可能是 null
  • 事件不是可靠消息队列,页面关闭时消息可能丢失;
  • localStorage 是同步 API,高频写入会阻塞主线程;
  • 不要把密码、Access Token 或隐私数据写到 localStorage 作为“消息中转站”。

它适合登录登出提示、主题切换、简单状态失效通知;复杂消息建议优先用 BroadcastChannel,兼容性不足时再降级到 storage 事件。

StorageEvent 变化通知示意

四、postMessage:窗口、弹窗和 iframe 的跨源通信

window.postMessage() 是专门用于跨源窗口通信的 API。发送方需要拿到目标窗口引用,例如:

const popup = window.open(
  'https://accounts.example.com/popup',
  'account-popup',
  'width=480,height=640'
)

popup?.postMessage(
  {
    type: 'auth-result',
    code: 'one-time-code'
  },
  'https://app.example.com'
)

接收方必须同时校验 origin、必要时校验 source,并验证消息结构:

window.addEventListener('message', event => {
  if (event.origin !== 'https://accounts.example.com') return
  if (event.source !== expectedPopup) return

  const message = event.data
  if (!message || message.type !== 'auth-result') return
  if (typeof message.code !== 'string') return

  exchangeOneTimeCode(message.code)
})

为什么不能随便使用 '*'

原文示例把 '*' 作为 iframe 的目标源,虽然演示方便,但生产代码应填写精确的目标 origin:

iframe.contentWindow.postMessage(
  { type: 'theme-changed', theme: 'dark' },
  'https://widget.example.com'
)

发送敏感数据时使用 '*' 可能让被导航后的窗口或恶意页面收到消息。接收端只检查 event.data 而不检查 event.origin,也可能让任意网站伪造管理员消息。

还要注意:event.origin 是发送时的源,目标窗口之后可能已经导航到另一个页面,所以对重要通信还应校验 event.source、握手随机数和会话状态。

window.opener 和安全

window.open() 返回被打开窗口的引用,被打开页面可能通过 window.opener 回传消息。对于不需要建立通信的外部链接,应使用:

<a href="https://external.example" target="_blank" rel="noopener noreferrer">
  打开外部网站
</a>

noopener 可以阻断新页面访问 opener,降低反向 Tabnabbing 风险。需要通信时,应明确设计 origin、source、握手和生命周期,不能把所有弹窗都放进“口口相传”的无限转发树中。消息转发还要有 messageId 和 visited 集合,防止循环和重复处理。

五、MessageChannel:创建一对专用端口

如果两个窗口或 Worker 已通过一次 postMessage 建立联系,可以传递一个 MessagePort,后续用专用端口通信:

const channel = new MessageChannel()

channel.port1.addEventListener('message', event => {
  console.log('收到回复:', event.data)
})
channel.port1.start()

iframe.contentWindow.postMessage(
  { type: 'connect', port: channel.port2 },
  'https://widget.example.com',
  [channel.port2]
)

channel.port1.postMessage({ type: 'ping' })

iframe 内:

window.addEventListener('message', event => {
  if (event.origin !== 'https://app.example.com') return
  if (event.data?.type !== 'connect') return

  const port = event.ports[0]
  if (!port) return

  port.onmessage = messageEvent => {
    if (messageEvent.data?.type === 'ping') {
      port.postMessage({ type: 'pong' })
    }
  }
})

MessageChannel 适合点对点协议和传输可转移对象;MessagePort 需要启动(使用 onmessage 赋值时通常会自动启动,使用 addEventListener 时显式调用 start() 更清晰)。

六、Service Worker:以它为中转站

Service Worker 可以通过 clients.matchAll() 找到它作用域内的页面,再调用 client.postMessage() 广播:

// service-worker.js
self.addEventListener('message', event => {
  const senderId = event.source?.id

  event.waitUntil(
    self.clients.matchAll({
      type: 'window',
      includeUncontrolled: true
    }).then(clients => {
      for (const client of clients) {
        if (client.id === senderId) continue
        client.postMessage(event.data)
      }
    })
  )
})

页面侧:

const registration = await navigator.serviceWorker.ready

navigator.serviceWorker.addEventListener('message', event => {
  handleMessage(event.data)
})

const controller = navigator.serviceWorker.controller
if (controller) {
  controller.postMessage({
    type: 'favorite-changed',
    articleId: 'article-42'
  })
} else {
  // 首次注册后可能还没有 controller;可以等待 controllerchange,
  // 或向 registration.active/等待中的 worker 发送消息。
  registration.active?.postMessage({ type: 'request-sync' })
}

Service Worker 的边界

  • 它只服务于注册作用域内的页面,不能天然给任意跨源页面广播;
  • 它有安装、激活、等待和终止生命周期,不是永远运行的后台进程;
  • 首次注册页面可能尚未被当前 Service Worker 控制,navigator.serviceWorker.controller 可以是 null
  • clients.matchAll() 默认不一定包含未控制页面,需要按需求设置 includeUncontrolled
  • Service Worker 适合和 Cache API、推送、离线策略组合,但不应把所有业务消息都塞进它;
  • 对敏感消息要验证页面来源、消息类型和权限,不要因为消息来自 Service Worker 就默认可信。

七、SharedWorker:多个页面共享一个 Worker

SharedWorker 允许同源的多个浏览上下文连接到同一个 Worker。Worker 通过 onconnect 得到每个页面的 MessagePort,可以维护连接列表并广播:

页面:

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

port.addEventListener('message', event => {
  console.log('来自 SharedWorker:', event.data)
})
port.start()

port.postMessage({
  type: 'subscribe',
  clientId: crypto.randomUUID()
})

shared-hub.js

const ports = new Set()

self.onconnect = event => {
  const port = event.ports[0]
  ports.add(port)
  port.start()

  port.addEventListener('message', messageEvent => {
    const message = messageEvent.data
    if (message?.type === 'close') {
      ports.delete(port)
      port.close()
      return
    }

    for (const target of ports) {
      if (target !== port) target.postMessage(message)
    }
  })
}

原文用“每秒轮询 SharedWorker 的 get 指令”实现同步,这可以展示思路,但会产生无意义轮询。SharedWorker 可以直接保存端口并广播;如果要保存最后状态,使用 IndexedDB,页面在 visibilitychange 或重新连接时读取一次。

SharedWorker 的浏览器支持和移动端场景要单独评估。它的生命周期由连接页面影响,页面进入后台、浏览器节流或设备资源紧张时也不能把它当作可靠服务器。

SharedWorker 多端口通信示意

八、IndexedDB:持久状态 + 通知,而不是高频轮询

IndexedDB 适合保存跨页面需要恢复的状态:

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

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

request.onsuccess = () => {
  const db = request.result
  const transaction = db.transaction('events', 'readwrite')
  transaction.objectStore('events').put({
    id: crypto.randomUUID(),
    type: 'favorite-changed',
    articleId: 'article-42',
    version: 8,
    createdAt: Date.now()
  })
}

更合理的组合是:

IndexedDB 保存最后状态/未完成任务
        +
BroadcastChannel 通知其他页面刷新
        +
页面重新打开或从后台恢复时再次读取 IndexedDB

不要用每秒轮询 IndexedDB 代替事件系统。若必须兼容缺少 BroadcastChannel 的环境,可以使用低频轮询并保存 lastSeenId,同时清理已处理事件,防止数据库无限增长。

九、Cookie 和服务器推送

Cookie 可以让多个同源页面看到服务端会话状态,但 Cookie 本身不是通用消息总线:

  • 页面 JavaScript 不能读取 HttpOnly Cookie;
  • Cookie 自动随请求发送,不适合承载高频消息;
  • 修改 Cookie 不会像 localStorage 那样为其他页面提供标准 storage 事件;
  • Cookie 的共享范围由 Domain/Path/SameSite 等属性决定,跨顶级域不能直接共享。

如果状态变化由服务端产生,可以使用:

  • SSE:服务器到浏览器的单向长连接,适合通知和进度;
  • WebSocket:双向长连接,适合实时协作和聊天;
  • WebTransport:更底层、更灵活,但部署和浏览器支持要求更高;
  • 轮询:最简单但有延迟和请求成本。

这些是服务器推送或实时通信,不应和“两个标签页之间的本地通信”混为一谈。一个页面从服务器收到消息后,可以再通过 BroadcastChannel 通知其他同源页面。

十、非同源页面如何通信

iframe bridge 非同源通信示意

非同源页面不能直接访问对方 DOM、localStorage 或 JavaScript 变量,但在双方都同意的情况下,可以使用 postMessage

// app.example.com
const frame = document.querySelector('#payment-frame')

window.addEventListener('message', event => {
  if (event.origin !== 'https://pay.example.net') return
  if (event.source !== frame.contentWindow) return
  if (event.data?.type !== 'payment-completed') return

  refreshOrder(event.data.orderId)
})

frame.contentWindow.postMessage(
  { type: 'ready' },
  'https://pay.example.net'
)

付款 iframe:

window.addEventListener('message', event => {
  if (event.origin !== 'https://app.example.com') return
  if (event.data?.type !== 'ready') return

  window.parent.postMessage(
    { type: 'payment-completed', orderId: 'order-42' },
    'https://app.example.com'
  )
})

原文提出“在每个页面嵌入同源 bridge iframe,再由 iframe 通过 BroadcastChannel 转发”。这在多个受控产品必须共用一个通信桥时仍可能有用,但通常直接使用 postMessage 更简单。桥接页面会扩大攻击面,必须:

  • 为每个消息方向维护精确的 allowlist;
  • 监听器先校验 originsource 和消息 schema;
  • 不使用 '*' 传输敏感内容;
  • 防止重放、循环转发和消息洪泛;
  • 不把 bridge 做成可以执行任意命令的“万能接口”。

十一、消息协议的工程化

跨页面通信真正难的部分通常不是调用 API,而是协议:

interface AppMessage {
  type: string
  messageId: string
  sentAt: number
  version: number
  payload: unknown
}

建议至少处理:

  • 消息类型白名单和运行时 schema 校验;
  • messageId 去重,避免重复消费;
  • version 兼容旧页面和新页面;
  • 是否需要 ACK、重试、超时和错误响应;
  • 页面隐藏、冻结、恢复和重新连接;
  • 消息是否需要持久化;
  • 不把完整用户对象、Token 和密码广播到所有标签页;
  • 发送方和接收方的权限边界。

登录状态同步可以只发送:

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

接收方收到后重新请求 /api/me,而不是广播 Access Token。

十二、方式对比

方式同源要求方向是否持久化适合场景
BroadcastChannel同源广播多标签页状态通知
storage 事件同源其他页面接收值在 Storage 中简单兼容方案
postMessage可跨源点对点iframe、弹窗、窗口
MessageChannel需先传递端口点对点专用通信端口
Service Worker同作用域页面/Worker 中转可配合 Cache/IDBPWA、页面广播、网络代理
SharedWorker同源多端口Worker 生命周期内多标签页共享 Worker
IndexedDB同源读写共享状态离线数据、任务状态
SSE/WebSocket网络可达服务端推送/双向需自行设计实时消息和协作
iframe bridge双方嵌入并同意跨源间接通信受控多产品桥接

总结

  1. 同源标签页之间优先考虑 BroadcastChannel,兼容性需要时再使用 storage 事件;
  2. iframe、弹窗和跨源页面使用 postMessage,必须校验 originsource 和消息结构;
  3. MessageChannel 适合建立专用点对点端口;
  4. Service Worker 和 SharedWorker 都有生命周期,不是永远在线的服务器;
  5. IndexedDB 应保存可恢复状态,消息通知和持久化状态最好组合使用;
  6. WebSocket/SSE 解决服务器推送,不等于浏览器本地跨标签页通信;
  7. window.opener 要结合 noopener、精确 origin 和消息去重;
  8. 跨页面通信要设计消息协议、错误、重试、权限和隐私,不能只关注 API 调用。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS