浏览器跨标签页通信不会?看了这篇文章你就会了!

前言

为了提高稳定性,每个标签页都是一个独立的浏览器上下文,它们之间是相互隔离的,一个标签页崩溃不会影响到其他标签页,无法直接访问对方的数据或进行通信。

浏览器跨标签页通信是指在同一浏览器的不同Tab标签页或窗口之间进行数据交互,通过跨标签页通信,可以实现数据的共享、状态的同步、消息的传递等功能。

简单说就是,所有的浏览器都支持多标签页的,我们可以在一个浏览器中打开多个标签页,每个标签页访问不同的网站内容,一个标签页能够发送信息给另一个标签页。已下图为例,当a.html标签页发送一个消息时,b.htmlc.html可以接收到来自a.html的消息,这就是跨标签页通信。

在日常项目开发中,虽然不经常用到,但还是很有价值的,遇到一些应用场景时可以信手拈来。

常见场景如下:

  1. 登录或登出:当用户打开多个标签页时,在某个标签页登录或登出需要通知其它所有标签页更新状态。
  2. 消息同步:当用户在一个标签页收到新的消息或通知时,需要将这一状态或消息内容传递给所有打开的页面,以实现实时提醒功能。
  3. 共享资源:在某些场景下,可能需要在多个标签页之间共享某些资源,如网络连接、音频/视频播放器等。

下面就来聊一聊实现浏览器跨标签页通信有哪些方式!

二、localStorage和sessionStorage事件

当一个页面修改了localStorage或sessionStorage,其它页面会收到一个事件提示,修改了localStorage或sessionStorage的页面不会收到这个事件。

// a.html
<button type="button" id="btn">点击</button>

<script type="text/javascript">
    const btn = document.getElementById('btn');
    btn.addEventListener('click', () => {
        localStorage.setItem('sendKey', "a向b发送了消息:吃饭了吗?")
        console.log("a向b发送了消息:吃饭了吗?")
    })
</script>

// b.html
window.addEventListener('storage', function(event) {
    console.log(event)
    if(event.key === 'sendKey') {
        const newValue = event.newValue;
        console.log(newValue)
        localStorage.setItem('sendKey', newValue)
    }
})

当 localStorage 或 sessionStorage 被修改时,将触发 storage 事件,判断对应 key 值是否发生变化。
当 localStorage 或 sessionStorage 不变时,不会触发 storage 事件。
此方法的操作简单,代码简单,不需要一些复杂的状态同步逻辑。

缺点如下:

  • 如果用户禁用了浏览的LocalStorage该方案就无效了。
  • 当前页的 setItem 不会触发当前页的 storage 事件,只会触发其它窗口的。
  • 要通信的标签页必须是同域的。

三、BroadcastChannel

Broadcast Channel,即广播频道接口,客户端通过创建 BroadcastChannel 对象加入广播频道。其构造函数只接受一个参数:频道的名称。如果它是第一个连接到该广播频道名称的客户端,则会创建底层频道。

const bc = new BroadcastChannel("test_channel");

发送消息:在创建的 BroadcastChannel 对象上调用 postMessage() 方法就足够了,该方法接受任何对象作为参数。

bc.postMessage("你吃饭了吗?");

接收消息:发布消息时,会向连接到此频道的每个 BroadcastChannel 对象发送一个 message 事件。可以使用 onmessage 事件处理器。

bc.onmessage = (event) => {
  console.log(event);
};

断开频道:要离开频道,请调用对象上的 close() 方法。这会断开对象与底层频道的连接,从而允许垃圾回收。

bc.close();

BroadcastChannel构造函数可以创建一个新的频道,然后各个标签页通过监听和发布消息进行通信。每个标签页都可以创建一个与之前频道同名的BroadcastChannel对象,然后就可以通过postMessage方法来进行发送消息,再通过onmessage事件来接受消息。

// a.html
<button type="button" id="btn">点击</button>

<script type="text/javascript">
    const btn = document.getElementById('btn');
    // 创建 BroadcastChannel 实例
    const bs = new BroadcastChannel('pay');
    // 点击事件
    btn.addEventListener('click', () => {
        // 发送消息
        bs.postMessage({ message: '支持成功,刷新状态' });
        // 关闭频道
        bs.close();
    })
</script> 
// b.html
// 创建 BroadcastChannel 实例
const bs = new BroadcastChannel('pay');
// 监听频道的消息
bs.onmessage = (event) => {
    console.log(event)
} 

适用于在同一域下的多个窗口、标签页或 iframe 之间进行实时消息广播。

四、SharedWorker

SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域。

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

创建一个执行指定 url 脚本的共享 web worker。

const myWorker = new SharedWorker("./worker.js"); 

如果已经用 addEventListener 监听了 onmessage 事件,则可以使用 start() 方法手动启动端口。

myWorker.port.start();
// a.html
<button type="button" id="btn">点击</button>
  
<script type="text/javascript">
    const btn = document.getElementById('btn');
    // 创建一个 SharedWorker
    const worker = new SharedWorker('./worker.js');

    btn.addEventListener('click', function(e) {
        // port.postMessage发送消息
        worker.port.postMessage({ 
            status: 'SUCCESS',
            message: '支付成功'
        });
    })
</script>

创建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);
      });
    };
};

b.html

// b.html
const worker = new SharedWorker('./worker.js');
const port = worker.port;
// onmessage接收消息
port.onmessage = function (event) {
    console.log('接收:', event.data);
};

SharedWorker的特点:

  • SharedWorker多页面共享线程只限于同一浏览器,而不能跨浏览器。
  • 共享实例,多个上下文共享一个worker实例,节省资源。
  • 同源策略,SharedWorker 连接到多个不同的页面必须是同源的。
  • 双向通信,可以在当前页面通过portMessage发送消息,也可以在当前页面通过onmessage接收消息。

五、Cookie

Cookies 是存储在用户计算机上的小文件,它们通常用于存储用户的偏好设置、跟踪用户会话等。同一个域名下的不同标签页可以共享 Cookies 数据,从而实现跨标签页的通信。

在一个标签页中设置 Cookie,然后在其他标签页中读取该 Cookie,并根据其值执行相应的操作。

设置Cookie:

function setCookie(key, value, d) {
    const date = new Date();
    date.setTime(date.getDate() + d*24*60*60*1000);
    const expires = date.toUTCString();
    document.cookie = `${key}=${value};expires=${expires};path=/;domain=${window.location.host}`;
}
setCookie('message', 'SUCCESS', 7)

获取cookie:

function getCookie(cname) {
    const name = cname + "=";
    const cookieValue = document.cookie.split(';');
    for(let i = 0; i < cookieValue.length; i++) {
        const str = cookieValue[i].trim();
        if (str.indexOf(name) == 0) {
            return str.substring(name.length,str.length);
        } 
    }
    return "";
} 

getCookie('message'); //SUCCESS

由于我们不能实时监听到Cookie的变化,因此在接收消息的标签页中,我们可以创建一个setInterval定时器,每隔一段时间轮询 Cookie 中的数据。 通过解析和处理 Cookie,我们可以获取到存储的消息,并进行相应的处理。

由于 Cookie 的大小有限制,通常为几 KB。如果需要发送的消息较大,可能需要拆分成多个 Cookie 进行存储。

Cookie 默认只能在同一域名下共享。如果需要在不同域名下进行跨标签页通信,需要设置合理的域名domain和路径path。下面就重点介绍一下domain和path的作用。

Cookie的domain属性:表示cookie所在的域,默认为请求的地址,如网址为a.test.com/public/a.html,那么domain默认为a.test.com

如域Aa.test.com,域B为b.test.com,那么在域A生产一个令域A和域B都能访问的cookie就要将该cookiedomain设置为.test.com

如果要在域A生产一个令域A不能访问而域B能访问的cookie就要将该cookiedomain设置为b.test.com

Cookiepath属性:表示cookie所在的目录,默认为 /,就是根目录。如在同一个服务器上有目录 /public//public/dir1//public/dir2/,现设一个cookie A的path为 /public/cookie Bpath/public/dir1/,那么public下的所有页面都可以访问到 cookie A,而 /public//public/dir2/ 的子页面不能访问 cookie B。这是因为 cookie 能让其path路径下的页面访问。

六、WebSocket

可以利用服务端做为中介进行跨标签页通信。WebSocket 最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

WebSocket 使用一个长连接,在客户端和服务器之间保持持久的连接,从而可以实时地发送和接收数据。

sequenceDiagram
    客户端->>服务端: UPgrade:websocket
    Note right of 服务端: 这时还是HTTP协议,由客户端发起连接
    服务端-->>客户端:101 Switching Protocols
    Note over 客户端,服务端: 切换协议为WebSocket协议
    客户端->>服务端:发送数据
    服务端-->>客户端:发送数据
    服务端-->>客户端:发送数据

onopen: 客户端和服务器建立连接后触发,被称为客户端和服务器之间的初始握手。如果接收到open, 说明已经连接成功,可以进行通信了。

onmessage:接收到消息时触发。服务器发送给客户端的消息可包括纯文本消息,二进制数据(Blob消息或者ArrayBuffer消息)。

onerror:响应意外故障时触发,在错误之后总是会终止连接。

onclose:连接关闭时触发。一旦连接关闭后,客户端和服务端将不会再进行消息的收发。也可主动调用close()方法关闭连接。

// 创建WebSocket对象,并指定服务器的地址
const socket = new WebSocket("ws://your-websocket-server.com");

// 与服务器建立连接:发送消息到服务器
socket.onopen = () => {
  console.log("connect: ");
};
// 收到服务器发送的消息:event处理服务器返回的数据
socket.onmessage = (event) => {
  console.log("receive: ", event.data);
};
// 连接或通信过程中发生错误
socket.onerror = (event) => {
  console.log("errror: ", event.error);
};
// 与服务器断开连接
socket.onclose = (event) => {
  console.log("close: ", event.code);
};

WebSocket的应用场景:

WebSocket主要应用于需要实时、双向通信的web应用中,如:

  • 即时聊天:构建实时聊天应用,用户可以实时发送和接收消息,实现低延迟、高效的在线交流。
  • 金融市场:股票、外汇、期货等金融市场的实时报价、交易提醒。
  • 新闻与社交媒体:实时推送通知。
  • 物联网(IoT):设备的状态监控与远程控制,如智能家居、工业自动化等。
  • 协作工具:在线文档编辑、白板绘图、代码协作等需要多方实时同步内容的应用。
  • 游戏:多人在线游戏中的实时状态同步、玩家交互。
  • 地理位置服务:实时位置追踪、导航应用中的动态路线更新。
  • 直播互动:直播平台的实时评论、弹幕、礼物赠送等互动功能。
  • 数据分析与监控:实时仪表盘、日志流处理、性能监控系统的实时数据展示与报警。

总结

选择通信方式:根据具体需求(如是否需要跨域、是否需要实时通信、通信数据的复杂程度等)选择合适的通信方式。

安全性:无论采用哪种方式,都需要注意数据的安全性,特别是当涉及到跨域通信时。

兼容性:考虑到不同浏览器的兼容性,选择兼容性较好的通信方式。

性能:对于需要频繁通信的场景,选择性能较好的通信方式,避免影响用户体验。

在实际应用中,可能会结合使用多种通信方式,以满足不同的需求。例如,可以使用Broadcast Channel进行实时通知,而使用LocalStorage进行简单的数据同步。

关于我
loading