注册
web

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

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


1_vCKb_XLed3eD9y4h-yjdKQ.gif


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


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


1_KJHO9DmEDcTISWuCcvDpMQ.gif


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


1. 服务器


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


2. 本地存储


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


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


image.png


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


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


3. 共享 Workers


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


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


image.png


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


image.png


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.screenXwindow.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 进行偏移。


image.png


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);



作者:StriveToY
来源:juejin.cn/post/7329753721018269711

0 个评论

要回复文章请先登录注册