代码、游戏、以及有趣的故事
2023-06-18
所谓多窗口下进行互相通信,是指在浏览器中,不同窗口(包括不同标签页、不同浏览器窗口甚至不同浏览器实例)之间进行数据传输和通信的能力。
当然,这里我们探讨的是纯前端的跨 Tab 页面通信,在非纯前端的方式下,我们可以借助诸如 WebSocket 等方式,藉由后端这个中间载体,进行跨页面通信。
为了实现跨窗口通信,它应该需要具备以下能力:
Broadcast Channel 是一个较新的 Web API,用于在不同的浏览器窗口、标签页或框架之间实现跨窗口通信。它基于发布-订阅模式,允许一个窗口发送消息,并由其他窗口接收。
其核心步骤如下:
同时,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>
思路具体如下:
createBroadcastChannel() 函数用于创建一个 BroadcastChannel 对象,并设置消息处理函数。sendMessage(data) 函数用于向 BroadcastChannel 发送消息。handleMessage(event) 函数用于处理接收到的消息。resizeEventBind() 函数用于监听窗口大小变化事件,并在事件发生时获取当前元素的位置信息,并通过 sendMessage() 函数发送位置信息到 BroadcastChannel。getCurPos() 函数用于计算当前元素相对于显示器窗口右上角的距离。在 onMounted() 生命周期钩子中,调用了 createBroadcastChannel() 和 resizeEventBind() 函数,用于在组件挂载后执行相关的初始化操作。
基于 BroadcastChannel,就可以实现每个 Tab 内的核心信息互传, 可以得知当前在线设备数,再基于这些信息去完成我们想要的动画、交互等效果。
其本质就是一个数据共享池子。核心点还是在于:1、数据向其他 Tab 页面传递的能力;2、Tab 页面接受其他页面传递过来的数据的能力。
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();
};
综上全部代码解释一下思路
event.ports[0] 获取到与 SharedWorker 建立的连接的第一个端口对象,并将其添加到 connections 数组中,表示该页面与共享 Worker 建立了连接。port.start() 启动端口对象,使其开始接收消息。总而言之,shared-worker.js 脚本创建了一个共享 Worker 实例,它可以接收来自不同页面的连接请求,并将接收到的消息发送给其他连接的页面。通过使用 SharedWorker API,实现跨 TAB 页面之间的通信和数据共享。
在 SharedWorker 方式中,传输数据与 Broadcast Channel 是一样的,都是利用 Message Event。简单对比一下:
shared-worker.js;最后一种跨 Tab 窗口通信的方式是利用 localStorage 、sessionStorage 本地化存储 API 以及的 storage 事件。
window.addEventListener('storage', function(event) {}) 事件实现了 localStore 变化时候的数据监听;<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>
简单解析一下:
#j-main 的坐标值,通过 ID 标识当 Key,存入 localStorage 中window.addEventListener('storage', (event)=> {}) 监听 localStorage 的变化