6 种浏览器跨窗口通信方案 - 从实际案例出发

浏览器跨窗口通信,听上去挺有技术感的,但实现起来方案倒是挺多的。本文将从一个实际的业务开发场景,带你了解如何实现浏览器跨窗口通信。

业务场景

一个常规的业务列表页,页面中提供了一个新增功能,由于新增功能的表单项内容比较多,所以交互上使用新开一个窗口来完成。这时问题来了,在新增完成后,如何通知列表页面刷新列表数据,以便展示出刚才新增的那一条数据。

各位可以先自己在心中简单想想,如果让你实现这个功能,你会使用什么方案。

解决方案

window.opener

window.opener 代表的是打开当前窗口的那个窗口的引用,例如:在 window A 中打开了 window B,B.opener 返回 A。

有了这个引用关系,我们就可以实现跨窗口通信:

  1. 列表页设置刷新列表方法 window.refreshList = () => {}
  2. 列表页通过 window.open 或者 <a href="newUrl" target="_blank" rel="opener">新增</a> 打开新增功能页面。
  3. 用户完成新增表单填写并提交,通过调用 window.opener.refreshList 方法来刷新列表页面数据,并关闭当前页。

有人可能注意到了,在 a 标签中我们使用到了 rel="opener" 属性,为什么要设置这个属性呢?

rel 属性定义了所链接的资源与当前文档的关系,常见的属性值有:

  • opener: 打开的新页面 window.opener 会指向前一个页面的 window。
  • noopener: 和 opener 相对应, window.opener 为空。
  • noreferrer:打开新页面时请求头不会包含 Referer,比如你未设置 noreferrer 的情况下,从 antd 打开百度,百度页面请求头就有 Referer: https://ant.design/
  • nofollow: 主要用于 SEO,告诉搜索引擎忽略该链接。

主要关注 opener 和 noopener 属性,a 标签默认情况下 rel=noopener,这代表打开的新增页面的 window.opener 对象会为空,所以需要设置 rel=opener。

那么又有一个问题,为什么 a 标签默认是 rel=noopener, 因为 opener 存在安全漏洞,比如你以 opener 的方式打开了一个未知的新页面,这个新页面可以通过 window.opener.location.href = 'fake.com' 重定向你的页面。

而 window.open 默认情况下 rel=opener,故打开的新页面可以拿到 window.opener 对象,不过要是打开第三方未知网站,建议设置为 noopener, 比如 window.open('https://baidu.com', 'title', 'noopener,noreferrer')。

BroadcastChannel

BroadcastChannel API 顾名思义,为"广播频道",适用于在同一域名 下的多个窗口、标签页或 iframe 之间进行实时消息广播。它的使用也非常简单,我们也看下如何通过它来实现上面的业务场景。

列表页创建一个 BroadcastChannel 实例来监听消息:

js 复制代码
// 创建 BroadcastChannel 实例
const channel = new BroadcastChannel('myChannel');

// 监听广播通道的消息
channel.addEventListener('message', (event) => {
  console.log('接收:', event.data); // { action: 'refresh' }
})

新增功能页面同样创建一个 BroadcastChannel 实例,频道名称需要和列表页一致:

js 复制代码
// 创建 BroadcastChannel 实例
const channel = new BroadcastChannel('myChannel');
// 向广播通道发送消息
channel.postMessage({ action: 'refresh' });
// 关闭频道
channel.close()

可以看到 BroadcastChannel 的使用很简单,双方创建同名频道的 BroadcastChannel 实例,然后一方监听 message 事件,一方使用 postMessage 广播内容。

postMessage

对于 postMessage,我们最常用的方式应该就是当前页面和 iframe 的跨域消息通信了,其实它也能完成跨窗口通信,核心就是能拿到新窗口的 window 对象,这个在 window.opener 方案中我们就知道通过设置 rel="opener" 就可以办到。

列表页打开新窗口,并监听 message 事件:

html 复制代码
<a href="./add.html" target="_blank" rel="opener">新增</a>
<script>
  // 不同与 BroadcastChannel,这边是监听 window 上的 message 事件
  window.addEventListener("message", receiveMessage);
  function receiveMessage(event) {
    console.log('接收:', event.data); // { action: 'refresh' }
  }
</script>

新增功能页面使用 window.opener.postMessage 发送消息:

js 复制代码
window.opener?.postMessage({ action: 'refresh' }, '*');

至此我们已经完成了上面的业务需求,postMessage 的优势在于可以跨域。

MessageChannel

MessageChannel API 允许我们创建一个新的消息通道,并通过它的两个 MessagePort 实例属性发送数据,同时它也可以跨域通信。

不同于 BroadcastChannel 的广播,MessageChannel 只提供双向通信通道,不过它既可以像 postMessage 一样用于 iframe 通信,也可以用于 Web Worker 之间进行通信。

要用 MessageChannel 实现跨窗口通信,方式有点类似 postMessage, 打开新页面时需要设置 rel="opener"。

列表页初始化 MessageChannel 实例,并在 port1 上监听 message 事件:

js 复制代码
// 创建 MessageChannel 实例
window.messageChannel = new MessageChannel();
const port1 = window.messageChannel.port1;
// port1 监听 message 事件
port1.onmessage = function(event) {
  console.log('接收:', event.data); // { action: 'refresh' }
};

新增功能页面使用 window.opener.messageChannel 拿到列表的 MessageChannel 实例,并使用 port2 的 postMessage 方法往 port1 通道上发送消息:

js 复制代码
// 获取 MessageChannel 实例
const messageChannel = window.opener.messageChannel;
const port2 = messageChannel.port2;
// 往 port1 发送消息
port2.postMessage({ action: 'refresh' });

需要注意的是 MessagePort 对象如果使用 addEventListener 监听 message 事件,就需要调用下 port.start() 方法,使用 onmessage 则可以不需要。

storage 事件

当 localStorage 或 sessionStorage 被修改时,将触发 storage 事件,利用这个机制,我们也可以完成跨窗口通信。同时因为用的是 localStorage 或 sessionStorage 方式,所以页面必须是同一域名下。

值得注意的是,sessionStorage 并不能满足上面的业务需求,sessionStorage 要想触发 storage 事件,必须在同一窗口,也就是一般只在当前页和其加载的同域名 iframe 下使用。还有一点就是当前页的 setItem 不会触发当前页的 storage 事件,只会触发其他窗口的。

列表页监听 storage 事件,判断是否是对应 key 值发生变化:

js 复制代码
window.addEventListener("storage", () => {
  console.log('发生变化的值:', event.key); 
  if (event.key === 'refresh') {
     // 刷新列表
  }
});

新增功能页面使用 localStorage 的 setItem 来触发列表的 storage 事件:

js 复制代码
window.localStorage.setItem('fresh', Date.now())

SharedWorker

SharedWorker 是 Web Workers API 的一种扩展,它允许在多个浏览器上下文中(例如多个页面或多个 iframe )共享一个 Worker。ShareWorker 遵守同源策略,也就是必须在同一域名下使用 SharedWorker。

先写个 worker.js 脚本:

js 复制代码
const ports = [];

onconnect = function (e) {
  const port = e.ports[0];
  ports.push(port);
  port.onmessage = function (e) {
    console.log("worker接收到的消息:", e.data);
    ports.forEach((p) => {
      p.postMessage(e.data);
    });
  };
};

列表页创建 ShareWorker 实例,然后监听 message 事件:

js 复制代码
const sharedWorker = new SharedWorker('./worker.js', 'test');
const port = sharedWorker.port;
port.onmessage = function (event) {
  console.log('接收:', event.data); // { action: 'refresh' }
};

新增功能页面使用 postMessage 发送消息给 worker,worker 在发送给各个主线程:

js 复制代码
const sharedWorker = new SharedWorker('./worker.js', 'test');
const port = sharedWorker.port;
port.postMessage({ action: 'refresh' });

这样我们就完成了上述的业务需求。

总结

上述的各种方式都可以实现通知列表页去做刷新动作,不过更推荐使用 window.opener 或 BroadcastChannel 来实现,这两种方式相对使用简单并且很符合这个业务场景。

对于其他需要跨窗口通信的场景,可以根据各个 API 的能力特点来选择使用哪个。

相关推荐
Csvn10 分钟前
SSR/SSG/ISR(上):峡谷与渲染策略的时间预算
前端
Csvn15 分钟前
SSR/SSG/ISR(下):可交互性、新鲜度与回源账
前端
酷炫码神1 小时前
Day 10|Pinia 状态管理:把散落在各组件的数据,收进一个全局仓库
android·开发语言·javascript
Elcker3 小时前
YnuoAgent(依诺):一个 npm 包,跑起属于自己的 AI 研发平台
前端·人工智能·npm
酷炫码神3 小时前
Day 8|Vue3 响应式:告别手动操作 DOM,让数据驱动界面
javascript·vue.js·ecmascript
计算机魔术师3 小时前
发现2天,修复2个月:AI挖穿PHP后,卡兹克讲透攻防时间差
前端
风骏时光牛马3 小时前
测试标题001
前端
IT_陈寒4 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记4 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
计算机魔术师4 小时前
半年前说不缺钱,半年后150亿美元超募
前端