所谓多窗口下进行互相通信,是指在浏览器中,不同窗口(包括不同标签页、不同浏览器窗口甚至不同浏览器实例)之间进行数据传输和通信的能力。

当然,这里我们探讨的是纯前端的跨 Tab 页面通信,在非纯前端的方式下,我们可以借助诸如 WebSocket 等方式,藉由后端这个中间载体,进行跨页面通信。

为了实现跨窗口通信,它应该需要具备以下能力:

Broadcast Channel

Broadcast Channel 是一个较新的 Web API,用于在不同的浏览器窗口、标签页或框架之间实现跨窗口通信。它基于发布-订阅模式,允许一个窗口发送消息,并由其他窗口接收。

其核心步骤如下:

  1. 创建一个 BroadcastChannel 对象:在发送和接收消息之前,首先需要在每个窗口中创建一个 BroadcastChannel 对象,使用相同的频道名称进行初始化。
  2. 发送消息:通过 BroadcastChannel 对象的 postMessage() 方法,可以向频道中的所有窗口发送消息。
  3. 接收消息:通过监听 BroadcastChannel 对象的 message 事件,可以在窗口中接收到来自其他窗口发送的消息。

同时,Broadcast Channel 遵循浏览器的同源策略。这意味着只有在同一个协议、主机和端口下的窗口才能正常进行通信。如果窗口不满足同源策略,将无法互相发送和接收消息。

<template>
    <div class="g-container" id="j-main">
        <!-- PASS -->
    </div>
</template>

<script>
import { onMounted } from 'vue';

export default {
    setup() {
        function createBroadcastChannel() {
            broadcastChannel = new BroadcastChannel('broadcast');
            broadcastChannel.onmessage = handleMessage;
        }

        function sendMessage(data) {
            broadcastChannel.postMessage(data);
        }

        function handleMessage(event) {
            console.log('接收到 event', event);
            // TODO: 处理接收到信息后的逻辑
        }

        function resizeEventBind() {
            window.addEventListener('resize', () => {
                const pos = getCurPos();
                sendMessage(pos);
            });
        }

        // 计算当前元素距离显示器窗口右上角的距离
        function getCurPos() {
            const barHeight = window.outerHeight - window.innerHeight;
            const element = document.getElementById('j-main');
            const rect = element.getBoundingClientRect();

            // 获取元素相对于屏幕左上角的 X 和 Y 坐标
            const x = rect.left + window.screenX; // 元素左边缘相对于屏幕左边缘的距离
            const y = rect.top + window.screenY + barHeight;// 元素顶部边缘相对于屏幕顶部边缘的距离

            return [x, y];
        }
        
        onMounted(() => {
            createBroadcastChannel();
            resizeEventBind();
        });

        return {};
    }
};
</script>

思路具体如下:

onMounted() 生命周期钩子中,调用了 createBroadcastChannel()resizeEventBind() 函数,用于在组件挂载后执行相关的初始化操作。

基于 BroadcastChannel,就可以实现每个 Tab 内的核心信息互传, 可以得知当前在线设备数,再基于这些信息去完成我们想要的动画、交互等效果。

其本质就是一个数据共享池子。核心点还是在于:1、数据向其他 Tab 页面传递的能力;2、Tab 页面接受其他页面传递过来的数据的能力。

SharedWorker API

SharedWorker API 是 HTML5 中提供的一种多线程解决方案,它可以在多个浏览器 TAB 页面之间共享一个后台线程,从而实现跨页面通信。

与其他 Worker 不同的是,SharedWorker 可以被多个浏览器 TAB 页面共享,且可以在同一域名下的不同页面之间建立连接。这意味着,多个页面可以通过 SharedWorker 实例之间的消息传递,实现跨 TAB 页面的通信。

<template>
  <div class="g-container" id="j-main">
    <!-- PASS -->
  </div>
</template>

<script>
import { onMounted } from 'vue';

export default {
    setup() {
        // 创建一个 SharedWorker 对象
        let worker;
        
        function initWorker() {
            // 创建一个 SharedWorker 对象
            worker = new SharedWorker('/shared-worker.js', 'tabWorker');

            // 监听消息事件
            worker.port.onmessage = function (event) {
                console.log('接收到 event', event);
                handleMessage(event);
            };
        }
        
        function handleMessage(data) {
            // TODO: 处理接收到信息后的逻辑
        }

        function sendMessage(data) {
            // 发送消息
            worker.port.postMessage(data);
        }

        function resizeEventBind() {
            window.addEventListener('resize', () => {
                const pos = getCurPos();
                sendMessage(pos);
            });
        }

        function getCurPos() {
            const barHeight = window.outerHeight - window.innerHeight;
            const element = document.getElementById('j-main');
            const rect = element.getBoundingClientRect();

            // 获取元素相对于屏幕左上角的 X 和 Y 坐标
            const x = rect.left + window.screenX; // 元素左边缘相对于屏幕左边缘的距离
            const y = rect.top + window.screenY + barHeight;// 元素顶部边缘相对于屏幕顶部边缘的距离

            return [x, y];
        }
        
        onMounted(() => {
            initWorker();
            resizeEventBind();
        });

        return {};
    }
};
</script>

initWorker() 方法中,使用 worker = new SharedWorker('/shared-worker.js', 'tabWorker') 创建了一个 SharedWorker , 后面每一个被打开的同域浏览器 TAB 页面,都是共享这个 Worker 线程,从而实现跨页面通信;
基于 worker.port.postMessage(data) 实现数据的传输;
基于 worker.port.onmessage = function() {} 实现传输数据的监听。

简易版 shared-worker.js 代码

//shared-worker.js
const connections = [];

onconnect = function (event) {
    var port = event.ports[0];
    connections.push(port);

    port.onmessage = function (event) {
        // 接收到消息时,向所有连接发送该消息
        connections.forEach(function (conn) {
            if (conn !== port) {
                conn.postMessage(event.data);
            }
        });
    };

    port.start();
};

综上全部代码解释一下思路

  1. 上面的代码中,定义了一个数组 connections,用于存储与 SharedWorker 建立连接的各个页面的端口对象;
  2. onconnect 是事件处理程序,当有新的连接建立时会触发该事件;
  3. 在 onconnect 函数中,通过 event.ports[0] 获取到与 SharedWorker 建立的连接的第一个端口对象,并将其添加到 connections 数组中,表示该页面与共享 Worker 建立了连接。
  4. 在连接建立后,为每个端口对象设置了 onmessage 事件处理程序。当端口对象接收到消息时,会触发该事件处理程序。
  5. 在 onmessage 事件处理程序中,通过遍历 connections 数组,将消息发送给除当前连接端口对象之外的所有连接。这样,消息就可以在不同的浏览器 TAB 页面之间传递。
  6. 最后,通过调用 port.start() 启动端口对象,使其开始接收消息。

总而言之,shared-worker.js 脚本创建了一个共享 Worker 实例,它可以接收来自不同页面的连接请求,并将接收到的消息发送给其他连接的页面。通过使用 SharedWorker API,实现跨 TAB 页面之间的通信和数据共享。

在 SharedWorker 方式中,传输数据与 Broadcast Channel 是一样的,都是利用 Message Event。简单对比一下:

localStorage API

最后一种跨 Tab 窗口通信的方式是利用 localStorage 、sessionStorage 本地化存储 API 以及的 storage 事件。

<template>
  <div class="g-container" id="j-main">
    // ...  
  </div>
</template>

<script>
import { ref, reactive, computed, onMounted } from 'vue';

export default {
    setup() {
        function initLocalStorage() {
            let tabArray = JSON.parse(localStorage.getItem('tab_array'));
            if (!tabArray) {
                const tabIndex = 1;
                id = tabIndex;
                localStorage.setItem('tab_array', JSON.stringify([tabIndex]));
            } else {
                const tabIndex = tabArray[tabArray.length - 1] + 1;
                id = tabIndex;
                const newTabArray = [...tabArray, tabIndex];
                localStorage.setItem('tab_array', JSON.stringify(newTabArray));
            }
        }

        function setLocalStorage(data) {
            localStorage.setItem(`tab_index_${id}`, JSON.stringify(data));
        }

        function handleMessage(data) {
            const rArray = JSON.parse(data);
            remoteX.value = rArray[0];
            remoteY.value = rArray[1];
        }

        function resizeEventBind() {
            window.addEventListener('resize', () => {
                const pos = getCurPos();
                setLocalStorage(pos);
            });
        
            window.addEventListener('storage', (event) => {
                console.log('localStorage 变化了!', event);
                console.log('键名:', event.key);
                console.log('变化前的值:', event.oldValue);
                console.log('变化后的值:', event.newValue);
                handleMessage(event.newValue);
            });
        }

        function getCurPos() {
            const barHeight = window.outerHeight - window.innerHeight;
            const element = document.getElementById('j-main');
            const rect = element.getBoundingClientRect();

            // 获取元素相对于屏幕左上角的 X 和 Y 坐标
            const x = rect.left + window.screenX; // 元素左边缘相对于屏幕左边缘的距离
            const y = rect.top + window.screenY + barHeight;// 元素顶部边缘相对于屏幕顶部边缘的距离

            return [x, y];
        }
        
        onMounted(() => {
            initLocalStorage();
            resizeEventBind();
        });

        return {};
    }
};
</script>

简单解析一下:

  1. 每次页面初始化时,都会首先有一个 initLocalStorage 过程,用于给当前页面一个唯一 ID 标识,并且存入 localStorage 中
  2. 每次页面 resize,将当前页面元素 #j-main 的坐标值,通过 ID 标识当 Key,存入 localStorage 中
  3. 其他页面,通过 window.addEventListener('storage', (event)=> {}) 监听 localStorage 的变化
近期文章
title: 页面进程详解
time: 2023-10-12
tag: #前端#浏览器#计算机科学
title: 域名解析
time: 2023-05-25
tag: #网络