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

前段时间看到了一张神奇的 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 实现自动推送

更多精彩详见:个人主页

相关推荐
万叶学编程2 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
前端李易安4 小时前
Web常见的攻击方式及防御方法
前端
PythonFun4 小时前
Python技巧:如何避免数据输入类型错误
前端·python
Neituijunsir4 小时前
2024.09.22 校招 实习 内推 面经
大数据·人工智能·算法·面试·自动驾驶·汽车·求职招聘
知否技术4 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
hakesashou4 小时前
python交互式命令时如何清除
java·前端·python
天涯学馆4 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF5 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi5 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器