JavaScript实现跨标签页通信


highlight: arduino-light
theme: channing-cyan

💡 在 Web 开发中,有时我们需要实现不同页面之间的数据传递和事件触发,比如一个页面打开了另一个页面,然后在新的页面中操作后需要更新原来的页面的内容。这种场景在电商、支付、社交等领域都很常见,那么如何用js来实现不同页面之间的交互呢?下面提供几种常见的方法供大家学习参考!

一、onStorage

localstorage是浏览器同域标签共用的存储空间,所以可以用来实现多标签之间的通信。html5出现了一个事件: onstorage,我们在window对象上添加监听就可以监听到变化:

window.addEventListener(‘storage’, (e) => console.log(e));所以在 Web Storage 中,每一次将一个值存储到本地存储时,都会触发一个 storage 事件,通过localStorage 结合window.addEventListener(‘storage’, cb) 完成 A、B 标签页间通信。

(1)代码实例

下面我们用具体的代码实际看一下:

需要注意,此事件是非当前页面对localStorage进行修改时才会触发,当前页面修改localStorage不会触发监听函数。如果实在是要,自己重写一个方法吧。

1、localStorage

<!-- 1.html -->
<script>
  if(!localStorage.getItem('a')){
		localStorage.setItem('a',1)
	}else{
		var sum = localStorage.getItem('a')
		localStorage.setItem('a', +sum + 1)
	}
</script>

<!-- 2.html  -->
<script>
  window.addEventListener('storage', (e) => console.log(e))
</script>

2、

sessionStorage.setItem('message', '我是sessionStorage的值');
// 触发自定义事件来通知其他标签页
var event = new Event('sessionStorageUpdated');
window.dispatchEvent(event);
 
window.addEventListener('storage', function(event) {
  if (event.key === 'message') {
    var message = sessionStorage.getItem('message');
    console.log('message:', message);
  }
});
 
// 监听自定义事件来检测 `sessionStorage` 的更新
window.addEventListener('sessionStorageUpdated', function() {
  var message = sessionStorage.getItem('message');
  console.log('message:', message);
});

(2)总结

我们新建两个html分别叫1.html和2.html,并加上上面的js,于是我们每次打开或者刷新该页面就会给a加上1。需要注意的是,如果是双击打开,是在file://协议下的,而且不会触发storage事件,但是会给a加上1,所以可以做一个功能,计算本地某个文件被打开了多少次。如果我们用服务器打开,我们的不同tab页面通信完成了,而且是实时的。
在这里插入图片描述

✍️Tips

  1. 该事件不在导致数据变化的当前页面触发(如果浏览器同时打开一个域名下面的多个页面,当其中的一个页面改变 数据时,其他所有页面的 storage 事件会被触发,而原始页面并不触发 storage 事件)。
  2. sessionStorage(❎)不能触发 storage 事件 , localStorage(✅)可以。
    ● 修正:localStorage 和 sessionStorage 对象都会触发 storage 事件,当其他窗口或标签页对存储进行更改时会触发该事件。localStorage 存储的数据是共享的,因此需要小心处理数据的同步和更新,以避免冲突和数据不一致的问题。storage 事件在更新同一 sessionStorage 的标签页之间不会触发。storage 事件无法提供更改的具体值,因此我们需要手动读取 sessionStorage 的值来获取更新的数据。sessionStorage 实现跨Tab页面的通信没有localStorage 那样直接和简单。
    ● sessionStorage 的作用域限定在当前会话中,关闭窗口或标签页后数据将被清除。即使是同一域名的不同窗口或者标签,他们之间的sessionStorage 数据也是相互隔离的,无法直接共享。每个窗口或者标签都有自己独立的sessionStorage 对象,他们之间的数据互不干扰。
  3. 如果修改的值未发生改变,将不会触发 onstorage 事件。
  4. 优点:浏览器支持效果好、API直观、操作简单。缺点:部分浏览器隐身模式下,无法设置 localStorage。如safari,这样也就导致 onstrage 事件无法使用。

除开少数情况,localStorage的兼容性不错,就当前国内的情况,已经基本没有问题了。localStorage 的原理很简单,浏览器为每个域名划出一块本地存储空间,用户网页可以通过 localStorage 命名空间进行读写。

二、BroadcastChannel

BroadcastChannel通信方式的原理就是一个命名管道(频道),可以让指定 origin 下的任意 browsing context 来订阅它。它允许同源的不同浏览器窗口,Tab页,frame或者 iframe 下的不同文档之间相互通信。通过触发一个 message 事件,消息可以广播到所有监听了该频道的BroadcastChannel 对象。

说到 BroadCast Channel 不得不说一下 postMessage,他们二者的最大区别就在于 postMessage 更像是点对点的通信,而 BroadCast Channel 是广播的方式,点到面。每个 BroadcastChannel 对象都需要使用一个唯一的名称来标识通道,这个名称在同一域名下的不同页面之间必须是唯一的,它允许同一域名下的不同页面之间进行通信。通过 postMessage 方法,一个页面可以将消息发送到频道中,而其他页面则可以监听 message 事件来接收这些消息。通过这种方式是短线了一种实时通信的机制,可以在不同的页面之间传递信息,实现页面间的即时交流。如下图所示:

(1)示例图

(2)代码实例

<!-- 示例1 -->
<!-- A.html -->
<script>
  const bc = new BroadcastChannel('test_channel');
  bc.postMessage('This is a test message.');
</script>
<!-- B.html -->
<script>
  const bc = new BroadcastChannel('test_channel');
  bc.onmessage = (event) => {
    console.log(event);
  };
</script>

<!-- 示例2 -->
<!-- 1.html -->
<body>
    <button id="l-btn">发送消息</button>
    <button id="s-btn">关闭</button>
    <script>
      // 创建
      const broadcastChannel = new BroadcastChannel('channelName');

      // 监听消息
      broadcastChannel.onmessage = function (e) {
        console.log('监听消息:', e.data);
      };

      document.getElementById('l-btn').onclick = function () {
        // 发送消息
        broadcastChannel.postMessage('测试,传送消息,我发送消息啦。。。');
      };

      document.getElementById('s-btn').onclick = function () {
        // 关闭
        broadcastChannel.close();
      };
  </script>
</body>
<!-- 2.html -->
<script>
  // 创建
  const broadcastChannel = new BroadcastChannel('channelName');

  // onmessage监听消息
  broadcastChannel.onmessage = function (e) {
    console.log('我是通过onmessage监听的消息:', e.data);
  };
  // addEventListener 监听
  broadcastChannel.addEventListener('message', function (e) {
    console.log('我是通过addEventListener监听的消息:', e)
  })
</script>

✍️Tips

  1. 监听消息除了 .onmessage 这种方式,还可以 使用addEventListener来添加’message’监听,
  2. 关闭除了使用 Broadcast Channel 实例为我们提供的 close 方法来关闭 Broadcast Channel。我们还可取消或者修改相应的’message’事件监听。两者是有区别的:取消’message’监听只是让页面不对广播消息进行响应,Broadcast Channel 仍然存在;而调用 close 方法会切断与 Broadcast Channel 的连接,浏览器才能够尝试回收该对象,因为此时浏览器才会知道用户已经不需要使用广播频道了。
  3. 兼容性:如果不使用 IE 和 sf on iOS 浏览器,兼容性还是可以的。

三、postMessage

postMessage 是 H5 引入的 API,该方法允许来自不同源的脚本采用异步方式进行有效的通信,可以实现跨文本文档、多窗口、跨域消息传递,多用于窗口间数据通信,这也使它成为跨域通信的一种有效的解决方案。

(1)语法

(2)代码实例

<!-- 1.html -->
<body>
    <button class="pop">弹出新窗口</button>
    <button class="button">发送数据</button>
    <script>
      const pop = document.querySelector('.pop');
      const button = document.querySelector('.button');

      let index = 0;
      let opener = null;

      pop.addEventListener('click', () => {
        opener = window.open(
          '2.html',
          '123',
          'height=600,width=600,top=20,resizeable=yes'
        );
      });

      button.addEventListener('click', () => {
        const data = {
          value: `moment ${index++}`,
        };

        opener.postMessage(data, '*');
      });
    </script>
  </body>

  <!-- 2.html -->
  <body>
  <div>postMessage  2.html</div>
  <script>
    window.addEventListener("message", (e) => {
      console.log(e.data);
    });
  </script>
</body>

(3)总结

通过点击按钮在主窗口和弹出的新窗口之间进行通信。通过 postMessage,主窗口可以向新窗口发送数据,从而实现了简单的跨窗口通信。在实际应用中,你可以在接收消息的窗口中监听 message 事件,然后在事件处理程序中处理接收到的数据。

具体代码运行效果如下图所示:
在这里插入图片描述

四、open & opener

当我们 系统中通过 window.open 打开一个新页面时,window.open 方法会返回一个被打开页面的引用,而被打开页面则可以通过 window.opener 获取到打开它的页面的引用(当然这是在没有指定noopener的情况下)。

✍️「扩展」- noopener

我们在系统中经常会这样使用 a 标签跳转到第三方网站,有时,当您单击网站上的链接时,该链接将在新选项卡中打开,但旧选项卡也会被重定向到其他网络钓鱼网站,它会要求您登录或开始将一些恶意软件下载到您的设备。这样存在一定的安全隐患,此时在新打开的页面中可通过 window.opener 获取到源页面的 window 对象, 这就埋下了安全隐患。 比如:

  • 你自己的网站 A,点击如上链接打开了第三方网站 B。
  • 此时网站 B 可以通过 window.opener 获取到 A 网站的 window 对象。
  • 然后通过 window.opener.location.href = ‘www.baidu.com’ 这种形式跳转到一个钓鱼网站,泄露用户信息。

为了避免这样的问题,可以添加引入了 rel=“noopener” 属性, 这样新打开的页面便获取不到来源页面的 window 对象了, 此时 window.opener 的值是 null。

但是由于一些老的浏览器并不支持 noopener ,通常 noopenernoreferrer 会同时设置, rel=“noopener noreferrer” 。


回到主题,使用 window.opener 如何实现跨页面通信!

(1)代码实例

  • 发送消息

  • 收集\获取消息

(2)总结

完整代码示例:

<!-- 1.html -->
<body>
    <button id="tab">新开 Tab</button>
    <button id="l-btn">发送消息</button>
    <script>
      // 单个
      // 发送消息:单个页面
      // const pop = document.querySelector('#tab');
      // const button = document.querySelector('#l-btn');
      // const data = {};
      // let windowOpen = null;
      // pop.addEventListener('click', () => {
      //   windowOpen = window.open(
      //     'tab.html',
      //     '123',
      //     'height=600,width=600,top=20,resizeable=yes'
      //   );
      // });

      // button.addEventListener('click', () => {
      //   data.message = '测试,传送消息,我发送消息啦。。。';
      //   windowOpen.postMessage(data, '*');
      // });

      // // 收集 window 对象:多个打开页面,打开一个页面就需要将打开的 window 对象收集起来,以便于发布广播
      let windowOpens = [];
      document.getElementById('tab').onclick = function () {
        // IP 地址为本地的服务
        const windowOpen = window.open(
          'tab.html'
        );
        windowOpens.push(windowOpen);
      };

      document.getElementById('l-btn').onclick = function () {
        const data = {};
        console.log(windowOpens);
        // 发送消息之前,先进行已关闭 Tab 的过滤
        windowOpens = windowOpens.filter((window) => !window.closed);

        if (windowOpens.length > 0) {
          // 数据打一个标记
          data.tag = false;
          data.message = '测试,传送消息,我发送消息啦。。。';
          windowOpens.forEach((window) => window.postMessage(data, '*'));
        }
        if (window.opener && !window.opener.closed) {
          data.tag = true;
          window.opener.postMessage(data, '*');
        }
      };
    </script>
  </body>

  <!-- tab.html -->
  <body>
    <script>
      // 多个
      let windowOpens = [];
      window.addEventListener('message', function (e) {
        const data = e.data;
        console.log('我接受到消息了:', data.message);
        // 避免消息回传
        if (window.opener && !window.opener.closed && data.tag) {
          window.opener.postMessage(data);
        }
        // 过滤掉已经关闭的 Tab
        windowOpens = windowOpens.filter((window) => !window.closed);
        // 避免消息回传
        if (windowOpens && !data.tag) {
          windowOpens.forEach((window) => window.postMessage(data));
        }
      });
    </script>
  </body>

✍️Tips

  1. 在收集到的 window 对象中,可能有的 Tab 窗口被关闭了,这种情况下的 Tab 不需要进行消息传递。
  2. 对于接受消息的一方来说,需要继续传递消息,但是这里存在一个问题就是消息回传,可能出现两者之间消息的死循环传递。
  3. 这种方式,类似击鼓传花,一个传一个,传递的消息从前往后,一条锁链。
  4. 但是如果页面不是通过一个页面打开的,而且直接打开的,或者从三方网站跳转的,那这条锁链将断开。 所以这种方式基本只做了解,问题太多,可不做参考。

五、SharedWorker

Shared Worker 是一种在多个浏览器标签页之间共享的 JavaScript 线程。它可以用于实现跨标签页的通信。

SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,如不同的浏览器标签页之间共享数据和执行代码。它可以用于在多个浏览上下文之间建立通信通道,以便它们可以共享信息和协同工作。例如几个窗口、iframe 或其他 worker。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域, SharedWorkerGlobalScope。与普通的 Worker 不同,SharedWorker 可以在多个浏览上下文中实例化,而不仅限于一个单独的浏览器标签页或框架。这使得多个浏览上下文可以共享同一个后台线程,从而更有效地共享数据和资源,而不必在每个标签页或框架中都创建一个独立的工作线程。

(1)代码实例

<!-- a.html -->
<script>
  let index = 0;
  const worker = new SharedWorker("worker.js");

  setInterval(() => {
    worker.port.postMessage(`moment ${index++}`);
  }, 1000);
</script>

<!-- b.html -->
<script>
  const worker = new SharedWorker("worker.js");

  worker.port.start();
  setInterval(() => {
    worker.port.postMessage("php是世界上最好的语言");
  }, 1000);

  worker.port.onmessage = function (e) {
    if (e.data) {
      console.log(e.data);
    }
  };
</script>

创建一个 worker.js 文件,并编写以下代码:

let data = "";

self.onconnect = (e) => {
  const port = e.ports[0];

  port.onmessage = function (e) {
    if (e.data === "php是世界上最好的语言") {
      port.postMessage(data);
      data = "";
    } else {
      data = e.data;
    }
  };
};

(2)总结

✍️Tips

备注: 如果要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源的(相同的协议、host 以及端口)。

  • Shared Worker 的最大问题在于实现跨页面通信时的,它无法主动通知所有页面,需要刷新页面或者是定时任务来检查是否有新的消息,也就是需要配合轮询来使用。
  • sharedWorker.js 不能使用 .addEventListener 来监听 message 事件,监听无效。 兼容性一般。

六、Service Worker

Service Worker 它是一种服务工作线程,是一种在浏览器背后运行的脚本,用于处理网络请求和缓存等任务。它是一种在浏览器与网络之间的中间层,允许开发者拦截和控制页面发出的网络请求,以及管理缓存,从而实现离线访问、性能优化和推送通知等功能。

它在浏览器背后独立运行与网页分开,这意味着即使用户关闭了网页,Service Worker 仍然可以运行。可以用于实现推送通知功能。它可以注册为推送消息的接收者,当服务器有新的通知要发送时,Service Worker 可以显示通知给用户,即使网页没有打开。

(1)代码实例

要想使用,首先我们创建两个不同的 html 文件分别代表不同的页面,创建一个 Service Worker 文件,并且使用 live server 开启一个本地服务器:

<!-- a.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      navigator.serviceWorker.register("worker.js").then(() => {
        console.log("注册成功");
      });

      setInterval(() => {
        navigator.serviceWorker.controller.postMessage({
          value: `moment ${new Date()}`,
        });
      }, 3000);

      navigator.serviceWorker.onmessage = function (e) {
        console.log(e.data.value);
      };
    </script>
  </body>
</html>

<!-- b.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      navigator.serviceWorker.register("worker.js").then(() => {
        console.log("注册成功");
      });

      setInterval(() => {
        navigator.serviceWorker.controller.postMessage({
          value: `moment ${new Date()}`,
        });
      }, 3000);

      navigator.serviceWorker.onmessage = function (e) {
        console.log(e.data.value);
      };
    </script>
  </body>
</html>

创建一个 worker.js 文件并编写以下代码:

// worker.js
self.addEventListener("message", function (e) {
  e.waitUntil(
    self.clients.matchAll().then(function (clients) {
      if (!clients || clients.length === 0) {
        return;
      }
      clients.forEach(function (client) {
        client.postMessage(e.data);
      });
    })
  );
});

代码运行如下图所示:

(2)总结

Service Worker 将遵守以下生命周期:

  • 注册: 在网页的 JavaScript 代码中调用 navigator.serviceWorker.register() 方法来注册一个 Service Worker;
  • 安装: 当 Service Worker 文件被下载并首次运行时,会触发 install 事件。在 install 事件中,你可以缓存静态资源,如 HTML、CSS、JavaScript 文件,以便在离线时使用;
  • 激活: 安装成功后,Service Worker 并不会立即接管页面的网络请求。它需要等到之前的所有页面都关闭,或者在下次页面加载时才会激活();
  • 控制: 一旦 Service Worker 被激活,它就开始控制在其作用域内的页面。它可以拦截页面发出的网络请求,并根据缓存策略返回缓存的内容;
  • 更新: 当你更新 Service Worker 文件并再次注册时,会触发一个新的 install 事件。你可以在新的 install 事件中更新缓存,然后在下次页面加载时进行激活,以确保新的 Service Worker 被使用;
  • 解除注册: 如果你不再需要 Service Worker,可以通过调用 navigator.serviceWorker.unregister() 来解除注册;

Service Worker是一个由 promise 封装的对象,未初始化时是一个 pending 状态的,当成功注册之后会变成 fulfilled,并且对外暴露以下方法,如下图所示:

✍️Tips

  1. Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。所以本质上来说 Service Worker 并不自动具备“广播通信”的功能,需要改造 Service Worker 添加些代码,将其改造成消息中转站。在 Service Worker 中监听了message事件,获取页面发送的信息。然后通过 self.clients.matchAll() 获取当前注册了 Service Worker 的所有页面,通过调用每个的 postMessage 方法,向页面发送消息。这样就把从一处(某个Tab页面)收到的消息通知给了其他页面。
  2. 兼容性:IE 全军覆没,其他浏览器还行,整体来说一般。

七、cookie

Cookie 的话很熟悉了,它是一种在浏览器和服务器之间传递的小型文本文件,可以用于在多个标签页之间共享数据。可以使用 setInterval 定时轮询 Cookie 来实现跨标签页通信。下面是一个示例:

(1)代码实例

  • 在发送消息的标签页中:
// 设置一个 Cookie,将消息存储在 Cookie 中
let index = 0;
setInterval(() => {
  document.cookie = `supper=moment ${index++}`;
}, 1000);
  • 在接收消息的标签页中:
<script>
  console.log("cookie 的值为: ", document.cookie);

  setInterval(() => {
    console.log("cookie 的值发生了变化: ", document.cookie);
  }, 1000);
</script>

(2)总结

  • 跨域名通信:Cookie 默认只能在同一域名下共享。如果需要在不同域名下进行跨标签页通信,需要设置合适的域名和路径。
  • Cookie 大小限制:Cookie 的大小有限制,通常为几 KB。如果消息较大,可能需要拆分成多个 Cookie 进行存储。
  • 安全性考虑:Cookie 中的数据可以被用户和其他脚本访问和修改。因此,不适合存储敏感信息。 以上示例提供了一个基本的框架来演示如何使用 Cookie 实现跨标签页通信。在实际应用中,您可能需要更复杂的逻辑来处理跨标签页通信,并确保数据同步和一致性。

总结

在上面列举了几种前端跨页面通信的方式,当然对前端来说远远不止这五种方式,还有其他方案例如:使用 hashchange、indexDB、Websocket 都是可以的,当前只是列举了部分。对于同源页面,常见的方式包括:

  • 广播模式:Broadcast Channel / Service Worker / LocalStorage + StorageEvent
  • 共享存储模式:Shared Worker / IndexedDB / cookie
  • 口口相传模式:window.open + window.opener
  • 基于服务端:Websocket / Comet / SSE 等

而对于非同源页面,则可以通过嵌入同源 iframe 作为“桥”,将非同源页面通信转换为同源页面通信。

1、不论是 Broadcast Channel,还是 Service Worker ,或是 storage 事件,其都是“广播模式”:一个页面将消息通知给一个“中央站”,再由“中央站”通知给各个页面。且onstorage、 BroadcastChannel、 Service Worker、 Open&Opener、SharedWorker 都是针对同源的 Tab。

2、对于前端跨页面通信一般选择的解决方案是使用 onstorage,主要考量的三个方面:

  • 兼容性。浏览器支持度。
  • 通用性。能否覆盖需求、是否具有拓展性。
  • 便捷性。开发便捷程度。

其他方案在这三个方面来说都或多或少存在一些美中不足。

参考文献

1、window属性:onstorage_w3cschool

2、storage - Web API 接口参考 | MDN

3、面试官:前端跨页面通信,你知道哪些方法? - 掘金

4、各类“服务器推”技术原理与实例(Polling/COMET/SSE/WebSocket) - 掘金

5、Service Worker ——这应该是一个挺全面的整理-CSDN博客

6、SharedWorker - Web APIs | MDN

7、【Service_Worker_API DEV】

8、BroadcastChannel - Web APIs | MDN

好书推荐

用最少的时间,参透高效能人士的持续成功之路。

https://book.douban.com/subject/5325618/

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值