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

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

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

业务场景

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

图片图片

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

解决方案

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 实例来监听消息:

// 创建 BroadcastChannel 实例
const channel = new BroadcastChannel('myChannel');

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

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

// 创建 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 事件:

<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 发送消息:

window.opener?.postMessage({ action: 'refresh' }, '*');

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

MessageChannel

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

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

图片图片

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

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

// 创建 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 通道上发送消息:

// 获取 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 值发生变化:

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

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

window.localStorage.setItem('fresh', Date.now())

SharedWorker

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

先写个 worker.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 事件:

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

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

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

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

总结

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

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

责任编辑:武晓燕 来源: 栗子前端
相关推荐

2018-04-11 10:51:25

多线程进程主线程

2022-04-07 18:49:56

项目场景数据库

2019-01-17 10:58:52

JS异步编程前端

2012-01-04 16:14:17

2009-04-17 22:25:16

多核四核CPU

2022-05-24 10:43:02

延时消息分布式MQ

2019-12-03 12:16:36

物联网ZigBee蓝牙低功耗

2019-05-15 08:00:00

vue组件间通信前端

2013-12-06 14:57:24

浏览器

2009-03-12 10:11:00

综合布线规划网络布局

2010-05-31 10:11:02

2019-10-12 01:10:09

物联网无线技术IOT

2010-09-15 09:12:03

JavaScript浏览器兼容

2010-03-15 17:12:52

Python字典

2017-06-26 10:35:58

前端JavaScript继承方式

2018-01-12 10:25:48

Nginx信号集master

2011-12-08 15:40:16

UC浏览器

2009-02-11 09:46:00

ASON网络演进

2023-04-07 08:25:33

JavaScript浏览器映射

2010-09-14 14:18:09

CSS跨浏览器开发
点赞
收藏

51CTO技术栈公众号