本文翻译自 https://dev.to/notachraf/sharing-a-state-between-windows-without-a-serve-23an
最近,社交网络上呈现了一张 gif 动图,展现了比约恩·斯塔尔 (Bjorn Staal) 制作的一件令人惊叹的艺术作品。动图
我想从新创立它,但不足球体、粒子和物理的 3D 技能,我的指标是理解如何使一个窗口对另一个窗口的地位做出反馈。实质上,在多个窗口之间共享状态,我发现这是 Bjorn 我的项目中最酷的方面之一!因为无奈找到无关该主题的好文章或教程,我决定与您分享我的发现。让咱们尝试依据 Bjorn 的工作创立一个简化的概念验证 (POC)!动图
我做的第一件事就是列出我所晓得的在多个客户端之间共享信息的所有办法:
呃:服务器
显然,领有服务器(带有轮询或 Websocket)能够简化问题。然而,因为 Bjorn 在没有应用服务器的状况下实现了他的后果,所以这是不可能的。
localStorage
localStorage 实质上是浏览器键值存储,通常用于在浏览器会话之间保存信息。尽管通常用于存储身份验证令牌或重定向 URL,但它能够存储任何可序列化的内容。您能够在这里理解更多信息。我最近发现了一些乏味的本地存储 API,包含 storage 每当本地存储被同一网站的另一个会话更改时就会触发的事件。想发现新的 API 吗?订阅我的时事通信(收费!)
咱们能够通过将每个窗口的状态存储在本地存储中来利用这一点。每当一个窗口扭转其状态时,其余窗口将通过存储事件进行更新。这是我最后的想法,这仿佛是 Bjorn 抉择的解决方案,因为他在这里分享了他的 LocalStorage 管理器代码以及与 ThreeJs 一起应用它的示例。然而当我发现有代码能够解决这个问题时,我想看看是否还有其余办法……剧透正告:是的,有!
WebWorker
简略来说,工作线程实质上是在另一个线程上运行的第二个脚本。尽管它们无法访问 DOM(因为它们存在于 HTML 文档之外),但它们依然能够与您的主脚本进行通信。它们次要用于通过解决后台作业来卸载主脚本,例如预取信息或解决不太要害的工作(例如流日志和轮询)。
共享工作线程是一种非凡类型的 WebWorkers,它能够与同一脚本的多个实例进行通信,这使得它们对咱们的用例很乏味!好吧,让咱们间接进入代码!
设置工人如前所述,工作人员是具备本人的入口点的“第二脚本”。依据您的设置(TypeScript、捆绑程序、开发服务器),您可能须要调整 tsconfig、增加指令或应用特定的导入语法。我无奈涵盖应用 Web Worker 的所有可能办法,但您能够在 MDN 或互联网上找到信息。如果须要,我很乐意为本文撰写前传,具体介绍设置它们的所有办法!就我而言,我应用 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 通知咱们窗口绝对于显示器左上角的地位。咱们将有两种类型的音讯:每个窗口,每当其状态发生变化时,都会公布 windowStateChangedmessage 其新状态。工作人员将向所有其余窗口发送更新,以揭示他们其中一个窗口已更改。工作人员将发送 syncmessage 蕴含所有窗口状态的信息。咱们能够从一个看起来有点像这样的普通工人开始:// 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 的音讯类型进行了一些输出:// 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<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”属性无奈序列化,所以我将其字符串化并解析回来。因为我很懒,我不只是将窗口映射到更可序列化的数组:w.port.postMessage({
action: "sync",
payload: {allWindows: JSON.parse(JSON.stringify(windows)) },
} satisfies WorkerMessage);
当初是时候画货色了!
乏味的局部:绘画!
当然,咱们不会做简单的 3D 球体:咱们只会在每个窗口的核心画一个圆,并在球体之间画一条线!我将应用 HTML Canvas 的根本 2D 上下文进行绘制,但您能够应用您想要的任何内容。画一个圆,非常简单: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<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,
});
});
}
}
};
作为最初一步,咱们只须要定期检查窗口是否发生变化,如果发生变化则发送音讯 setInterval(() => {
const newWindow = getCurrentWindowState();
if (
didWindowChange({
newWindow,
oldWindow: currentWindow,
})
) {
sharedWorker.port.postMessage({
action: "windowStateChanged",
payload: {
id,
newWindow,
},
} satisfies WorkerMessage);
currentWindow = newWindow;
}
}, 100);
您能够在此存储库中找到残缺的代码。实际上,我用它做了很多试验,使它变得更加形象,但其要点是雷同的。如果您在多个窗口上运行它,心愿您能失去与此雷同的后果!动图
谢谢浏览!如果您发现这篇文章有帮忙、乏味或只是乏味,您能够将其分享给您的敌人 / 共事 / 社区