如何去实现浏览器多窗口互动

前段时间看到了一张神奇的 gif,如下:

感觉特别不可思议,而且是本地运行的环境,于是想自己实现一个但是碍于自己太菜了缺乏对球体、粒子和物理的3D技能,然后去了解了一下如何使一个窗口对另一个窗口的位置做出反应。

于是我做了一个极简的丑陋的版本:

首先,我们看一下在多个客户端之间共享信息的所有方法:

1. 服务器

显然,拥有服务器(使用轮询或Websockets)会简化问题。然而,我们能不能在不使用服务器的情况下去实现呢?

2. 本地存储

本地存储本质上是一个浏览器键值存储,通常用于在浏览器会话之间保持信息的持久性。虽然通常用于存储身份验证令牌或重定向URL,但它可以存储任何可序列化的内容。可以在这里了解更多信息

最近发现了一些有趣的本地存储API,包括storage事件,该事件在同一网站的另一个会话更改本地存储时触发。

我们可以通过将每个窗口的状态存储在本地存储中来利用这一点。每当一个窗口改变其状态时,其他窗口将通过存储事件进行更新。

这是我最初的想法,但是后来发现还有其他的方式可以实现

3. 共享 Workers

简单来说,Worker本质上是在另一个线程上运行的第二个脚本。虽然它们没有访问DOM,因为它们存在于HTML文档之外,但它们仍然可以与您的主脚本通信。 它们主要用于通过处理后台作业来卸载主脚本,比如预取信息或处理诸如流式日志和轮询之类的较不重要的任务。

我这有一篇关于web Worker 的文章 没了解过的可以先去看看。

共享的 Workers 是一种特殊类型的 WebWorkers,可以与多个相同脚本的实例通信。

4. 建立 Workers

我使用的是Vite和TypeScript,所以我需要一个worker.ts文件,并将@types/sharedworker作为开发依赖进行安装。我们可以使用以下语法在我的主脚本中创建连接:

js 复制代码
new SharedWorker(new URL("worker.ts", import.meta.url));

接下来需要考虑的就是以下几方面:

  • 确定每个窗口
  • 跟踪所有窗口的状态
  • 当一个窗口改变其状态时,通知其他窗口重新绘制
js 复制代码
type WindowState = {  
    screenX: number; // window.screenX  
    screenY: number; // window.screenY  
    width: number; // window.innerWidth  
    height: number; // window.innerHeight  
};

最关键的信息是window.screenXwindow.screenY,因为它们可以告诉我们窗口相对于显示器左上角的位置。

将有两种类型的消息:

  • 每个窗口在改变状态时,将发布一个windowStateChanged消息,带有其新状态。
  • 工作者将向所有其他窗口发送更新,以通知它们其中一个已更改。工作者将使用sync消息发送所有窗口的状态。
js 复制代码
// worker.ts  
let windows: { windowState: WindowState; id: number; port: MessagePort }[] = [];  
  
onconnect = ({ ports }) => {  
    const port = ports[0];  

    port.onmessage = function (event: MessageEvent<WorkerMessage>) {  
        console.log("We'll do something");  
    };  
};

我们与 SharedWorker 的基本连接将如下所示。我编写了一些基本函数来生成一个ID,并计算当前窗口状态,同时我对我们可以使用的消息类型进行了一些类型定义,称为 WorkerMessage

js 复制代码
// main.ts  
import { WorkerMessage } from "./types";  
import {  
    generateId,  
    getCurrentWindowState,  
} from "./windowState";  
  
const sharedWorker = new SharedWorker(new URL("worker.ts", import.meta.url));  
let currentWindow = getCurrentWindowState();  
let id = generateId();

一旦启动应用程序,应该立即通知工作者有一个新窗口,因此需要发送一条消息:

js 复制代码
// main.ts  
sharedWorker.port.postMessage({  
    action: "windowStateChanged",  
    payload: {  
        id,  
        newWindow: currentWindow,  
    },  
} satisfies WorkerMessage);

然后可以在工作者端监听此消息,并相应地更改 onmessage。基本上,一旦接收到 windowStateChanged 消息,它要么是一个新窗口,我们将其追加到状态中,要么是一个旧窗口发生了变化。然后,我们应该通知所有窗口状态已经改变:

js 复制代码
// worker.ts  
port.onmessage = function (event: MessageEvent<WorkerMessage>) {  
    const msg = event.data;  
    switch (msg.action) {  
        case "windowStateChanged": {  
            const { id, newWindow } = msg.payload;  
            const oldWindowIndex = windows.findIndex((w) => w.id === id);  
            if (oldWindowIndex !== -1) {  
                // old one changed  
                windows[oldWindowIndex].windowState = newWindow;  
            } else {  
                // new window  
                windows.push({ id, windowState: newWindow, port });  
            }  
                windows.forEach((w) =>  
                // send sync here  
            );  
        } 
        break;
    }  
};

要发送同步消息,实际上我需要一个小技巧,因为"port"属性无法被序列化,所以我将其转换为字符串,然后再解析回来。因为我比较懒,我不会只是将窗口映射到一个更可序列化的数组:

js 复制代码
w.port.postMessage({  
    action: "sync",  
    payload: { allWindows: JSON.parse(JSON.stringify(windows)) },  
} satisfies WorkerMessage);

接下来就是绘制内容了。

5. 使用Canvas 绘图

在每个窗口的中心画一个圆圈,并用一条线连接这些圆圈,将使用 HTML Canvas 进行绘制

有对Canvas 不太熟悉的同学可以去看我这两篇文档:

# canvas 爬坑路【属性篇】

# canvas 爬坑路【方法篇】

js 复制代码
const drawCenterCircle = (ctx: CanvasRenderingContext2D, center: Coordinates) => {  
    const { x, y } = center;  
    ctx.strokeStyle = "#eeeeee";  
    ctx.lineWidth = 10;  
    ctx.beginPath();  
    ctx.arc(x, y, 100, 0, Math.PI * 2, false);  
    ctx.stroke();  
    ctx.closePath();  
};

要绘制线条,需要进行一些数学计算(我保证,不是很多 🤓),将另一个窗口中心的相对位置转换为当前窗口上的坐标。 基本上,正在改变基底。使用以下数学公式来实现这个功能。首先,将更改基底,使坐标位于显示器上,并通过当前窗口的 screenX/screenY 进行偏移。

js 复制代码
const baseChange = ({  
    currentWindowOffset,  
    targetWindowOffset,  
    targetPosition,  
}: {  
    currentWindowOffset: Coordinates;  
    targetWindowOffset: Coordinates;  
    targetPosition: Coordinates;  
}) => {  
    const monitorCoordinate = {  
        x: targetPosition.x + targetWindowOffset.x,  
        y: targetPosition.y + targetWindowOffset.y,  
    };  
  
    const currentWindowCoordinate = {  
        x: monitorCoordinate.x - currentWindowOffset.x,  
        y: monitorCoordinate.y - currentWindowOffset.y,  
    };  
  
    return currentWindowCoordinate;  
};

现在有了相同相对坐标系上的两个点,可以画线了!

js 复制代码
const drawConnectingLine = ({  
    ctx,  
    hostWindow,  
    targetWindow,  
}: {  
    ctx: CanvasRenderingContext2D;  
    hostWindow: WindowState;  
    targetWindow: WindowState;  
}) => {  
    ctx.strokeStyle = "#ff0000";  
    ctx.lineCap = "round";  
    const currentWindowOffset: Coordinates = {  
    x: hostWindow.screenX,  
    y: hostWindow.screenY,  
    };  
    const targetWindowOffset: Coordinates = {  
    x: targetWindow.screenX,  
    y: targetWindow.screenY,  
    };  

    const origin = getWindowCenter(hostWindow);  
    const target = getWindowCenter(targetWindow);  

    const targetWithBaseChange = baseChange({  
    currentWindowOffset,  
    targetWindowOffset,  
    targetPosition: target,  
    });  

    ctx.strokeStyle = "#ff0000";  
    ctx.lineCap = "round";  
    ctx.beginPath();  
    ctx.moveTo(origin.x, origin.y);  
    ctx.lineTo(targetWithBaseChange.x, targetWithBaseChange.y);  
    ctx.stroke();  
    ctx.closePath();  
};

现在,只需要对状态变化做出反应即可。

js 复制代码
// main.ts  
sharedWorker.port.onmessage = (event: MessageEvent<WorkerMessage>) => {  
    const msg = event.data;  
    switch (msg.action) {  
        case "sync": {  
            const windows = msg.payload.allWindows;  
            ctx.reset();  
            drawMainCircle(ctx, center);  
            windows  
            .forEach(({ windowState: targetWindow }) => {  
                drawConnectingLine({  
                    ctx,  
                    hostWindow: currentWindow,  
                    targetWindow,  
                });  
            });  
        }  
    }  
};

最后一步,只需要定期检查窗口是否发生了变化,如果是,则发送一条消息。

js 复制代码
setInterval(() => {setInterval(() => {  
    const newWindow = getCurrentWindowState();  
    if (  
        didWindowChange({  
            newWindow,  
            oldWindow: currentWindow,  
        })  
    ) {  
    sharedWorker.port.postMessage({  
        action: "windowStateChanged",  
        payload: {  
            id,  
            newWindow,  
        },  
    } satisfies WorkerMessage);  
        currentWindow = newWindow;  
    }  
}, 100);

如果有兴趣的同学想自己看下效果可以去我的 github 有我上传的代码欢迎给个 star

点赞收藏支持、手留余香、与有荣焉,动动你发财的小手哟,感谢各位大佬能留下您的足迹。

往期热门精彩推荐

解锁 JSON.stringify() 5 个鲜为人知的功能

面试相关热门推荐

前端万字面经------基础篇

前端万字面积------进阶篇

简述 pt、rpx、px、em、rem、%、vh、vw的区别

实战开发相关推荐

前端常用的几种加密方法

探索Web Worker在Web开发中的应用

不懂 seo 优化?一篇文章帮你了解如何去做 seo 优化

【实战篇】微信小程序开发指南和优化实践

前端性能优化实战

聊聊让人头疼的正则表达式

获取文件blob流地址实现下载功能

Vue 虚拟 DOM 搞不懂?这篇文章帮你彻底搞定虚拟 DOM

移动端相关推荐

移动端横竖屏适配与刘海适配

移动端常见问题汇总

聊一聊移动端适配

Git 相关推荐

通俗易懂的 Git 入门

git 实现自动推送

更多精彩详见:个人主页

相关推荐
xiezhuangshunv2 分钟前
20250401-vue-声明触发的事件
前端·javascript·vue.js
PBitW21 分钟前
微信小程序 -- 原生封装table
前端·微信小程序
uhakadotcom24 分钟前
2025年春招:如何使用DeepSeek + 豆包优化简历,轻松敲开心仪公司的大门
算法·面试·github
掘金安东尼35 分钟前
用 Python 搭桥,Slack 上跑起来的 MCP 数字员工
人工智能·面试·github
cwtlw39 分钟前
java基础知识面试题总结
java·开发语言·学习·面试
小杨xyyyyyyy42 分钟前
JVM - 垃圾回收器常见问题
java·jvm·面试
小满zs1 小时前
React-router v7 第一章(安装)
前端·react.js
程序员小续1 小时前
前端低代码架构解析:拖拽 UI + 代码扩展是怎么实现的?
前端·javascript·面试
wangpq1 小时前
微信小程序地图callout气泡图标在ios显示,在安卓机不显示
前端·vue.js