前言
大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。
今天在其他地方浏览到了一篇文章,发现比较有趣,所以翻译成中文分享给大家。
正文
最近,网络上出现了一张 gif 动图,展示了比约恩·斯塔尔 (Bjorn Staal) 制作的一件令人惊叹的艺术作品。
我想重新创建它,但缺乏球体、粒子和物理的 3D 技能,我的目标是了解如何使一个窗口对另一个窗口的位置做出反应。
本质上,在多个窗口之间共享状态,我发现这是 Bjorn 项目中最酷的方面之一!由于无法找到有关该主题的好文章或教程,我决定与您分享我的发现。
让我们尝试根据 Bjorn 的工作创建一个简化的概念验证 (POC)!
我做的第一件事就是列出我所知道的在多个客户端之间共享信息的所有方法:
咄:服务器
显然,拥有服务器(带有轮询或 Websocket)可以简化问题。然而,由于 Bjorn 在没有使用服务器的情况下实现了他的结果,所以这是不可能的。
本地存储
本地存储本质上是浏览器键值存储,通常用于在浏览器会话之间保存信息。虽然通常用于存储身份验证令牌或重定向 URL,但它可以存储任何可序列化的内容。您可以在这里了解更多信息。
我最近发现了一些有趣的本地存储 API,包括*storage
*每当本地存储被同一网站的另一个会话更改时就会触发的事件。
我们可以通过将每个窗口的状态存储在本地存储中来利用这一点。每当一个窗口改变其状态时,其他窗口将通过存储事件进行更新。
这是我最初的想法,这似乎是 Bjorn 选择的解决方案,因为他在这里分享了他的 LocalStorage 管理器代码以及与 ThreeJs 一起使用它的示例。
但是当我发现有代码可以解决这个问题时,我想看看是否还有其他方法......剧透警告:是的,有!
Shared Workers
这个华而不实的术语背后是一个令人着迷的概念------WebWorkers 的概念。
简单来说,工作线程本质上是在另一个线程上运行的第二个脚本。虽然它们无法访问 DOM(因为它们存在于 HTML 文档之外),但它们仍然可以与您的主脚本进行通信。
它们主要用于通过处理后台作业来卸载主脚本,例如预取信息或处理不太关键的任务(例如流日志和轮询)。
共享工作线程是一种特殊类型的 WebWorkers,它可以与同一脚本的多个实例进行通信,这使得它们对我们的用例很有趣!好吧,让我们直接进入代码!
设置worker
如前所述,worker
是具有自己的入口点的"第二脚本"。根据您的设置(TypeScript、bundler、开发服务器),您可能需要调整 tsconfig、添加指令或使用特定的导入语法。
我无法涵盖使用 Web Worker 的所有可能方法,但您可以在 MDN 或互联网上找到信息。
如果需要,我很乐意为本文撰写前传,详细介绍设置它们的所有方法!
就我而言,我使用 Vite 和 TypeScript,因此我需要一个worker.ts
文件并将其安装@types/sharedworker
为开发依赖项。我们可以使用以下语法在主脚本中创建连接:
js
new SharedWorker(new URL("worker.ts", import.meta.url));
基本上,我们需要:
- 识别每个窗口
- 跟踪所有窗口状态
- 一旦窗口改变状态,提醒其他窗口重绘
我们的状态将非常简单:
ts
type WindowState = {
screenX: number; // window.screenX
screenY: number; // window.screenY
width: number; // window.innerWidth
height: number; // window.innerHeight
};
当然,最重要的信息是window.screenX
它们window.screenY
告诉我们窗口相对于显示器左上角的位置。
我们将有两种类型的消息:
- 每个窗口,每当其状态发生变化时,都会发布
windowStateChangedmessage
其新状态。 worker
将向所有其他窗口发送更新,以提醒他们其中一个窗口已更改。worker
将发送syncmessage
包含所有窗口状态的信息。
我们可以新建立一个简单的worker
开始:
ts
// 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 的消息类型进行了一些输入:
ts
// 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();
一旦我们启动应用程序,我们应该提醒worker
有一个新窗口,因此我们立即发送一条消息:
ts
// main.ts
sharedWorker.port.postMessage({
action: "windowStateChanged",
payload: {
id,
newWindow: currentWindow,
},
} satisfies WorkerMessage);
我们可以在工作端监听此消息并相应地更改 onmessage。基本上,一旦工作人员收到 windowStateChanged 消息,要么它是一个新窗口,我们将其附加到状态,要么它是一个已更改的旧窗口。然后我们应该提醒大家状态已经改变:
ts
// 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"属性无法序列化,所以我将其字符串化并解析回来。因为我很懒,我不只是将窗口映射到更可序列化的数组:
ts
w.port.postMessage({
action: "sync",
payload: { allWindows: JSON.parse(JSON.stringify(windows)) },
} satisfies WorkerMessage);
现在是时候画东西了!
有趣的部分:绘画!
当然,我们不会做复杂的 3D 球体:我们只会在每个窗口的中心画一个圆,并在球体之间画一条线!
我将使用 HTML Canvas 的基本 2D 上下文进行绘制,但您可以使用您想要的任何内容。画一个圆,非常简单:
ts
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 进行偏移
ts
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;
};
如您所知,现在我们在同一相对坐标系上有两个点,我们现在可以画线了!
ts
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();
};
现在,我们只需要对状态变化做出反应。
ts
// 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,
});
});
}
}
};
作为最后一步,我们只需要定期检查窗口是否发生变化,如果发生变化则发送消息
ts
setInterval(() => {
const newWindow = getCurrentWindowState();
if (
didWindowChange({
newWindow,
oldWindow: currentWindow,
})
) {
sharedWorker.port.postMessage({
action: "windowStateChanged",
payload: {
id,
newWindow,
},
} satisfies WorkerMessage);
currentWindow = newWindow;
}
}, 100);
您可以在此存储库中找到完整的代码。实际上,我用它做了很多实验,使它变得更加抽象,但其要点是相同的。
如果您在多个窗口上运行它,希望您能得到与此相同的结果!