如何去实现浏览器多窗口互动
前段时间看到了一张神奇的 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
作为开发依赖进行安装。我们可以使用以下语法在我的主脚本中创建连接:
new SharedWorker(new URL("worker.ts", import.meta.url));
接下来需要考虑的就是以下几方面:
- 确定每个窗口
- 跟踪所有窗口的状态
- 当一个窗口改变其状态时,通知其他窗口重新绘制
type WindowState = {
screenX: number; // window.screenX
screenY: number; // window.screenY
width: number; // window.innerWidth
height: number; // window.innerHeight
};
最关键的信息是window.screenX
和window.screenY
,因为它们可以告诉我们窗口相对于显示器左上角的位置。
将有两种类型的消息:
- 每个窗口在改变状态时,将发布一个
windowStateChanged
消息,带有其新状态。 - 工作者将向所有其他窗口发送更新,以通知它们其中一个已更改。工作者将使用sync消息发送所有窗口的状态。
// worker.ts
let windows: { windowState: WindowState; id: number; port: MessagePort }[] = [];
onconnect = ({ ports }) => {
const port = ports[0];
port.onmessage = function (event: MessageEvent) {
console.log("We'll do something");
};
};
我们与 SharedWorker
的基本连接将如下所示。我编写了一些基本函数来生成一个ID,并计算当前窗口状态,同时我对我们可以使用的消息类型进行了一些类型定义,称为 WorkerMessage
:
// 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();
一旦启动应用程序,应该立即通知工作者有一个新窗口,因此需要发送一条消息:
// main.ts
sharedWorker.port.postMessage({
action: "windowStateChanged",
payload: {
id,
newWindow: currentWindow,
},
} satisfies WorkerMessage);
然后可以在工作者端监听此消息,并相应地更改 onmessage
。基本上,一旦接收到 windowStateChanged
消息,它要么是一个新窗口,我们将其追加到状态中,要么是一个旧窗口发生了变化。然后,我们应该通知所有窗口状态已经改变:
// worker.ts
port.onmessage = function (event: MessageEvent) {
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
”属性无法被序列化,所以我将其转换为字符串,然后再解析回来。因为我比较懒,我不会只是将窗口映射到一个更可序列化的数组:
w.port.postMessage({
action: "sync",
payload: { allWindows: JSON.parse(JSON.stringify(windows)) },
} satisfies WorkerMessage);
接下来就是绘制内容了。
5. 使用Canvas 绘图
在每个窗口的中心画一个圆圈,并用一条线连接这些圆圈,将使用 HTML Canvas
进行绘制
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
进行偏移。
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;
};
现在有了相同相对坐标系上的两个点,可以画线了!
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();
};
现在,只需要对状态变化做出反应即可。
// main.ts
sharedWorker.port.onmessage = (event: MessageEvent) => {
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,
});
});
}
}
};
最后一步,只需要定期检查窗口是否发生了变化,如果是,则发送一条消息。
setInterval(() => {setInterval(() => {
const newWindow = getCurrentWindowState();
if (
didWindowChange({
newWindow,
oldWindow: currentWindow,
})
) {
sharedWorker.port.postMessage({
action: "windowStateChanged",
payload: {
id,
newWindow,
},
} satisfies WorkerMessage);
currentWindow = newWindow;
}
}, 100);
来源:juejin.cn/post/7329753721018269711