前端量子纠缠效果——浏览器跨 Tab 窗口通信原理及应用

387 阅读6分钟

前端量子纠缠效果——浏览器跨 Tab 窗口通信原理及应用

twitter.com/i/status/17…

最近刷b站时候看到分享的一个视频,有个国外大佬用前端做出来一个可以跨浏览器窗口实时交互的渲染效果。看了一下原文推特下的评论,看起来是用到了localstoragewindow.screenXwindow.screenY

思路是用canvas在页面窗口中画一个圆,通过window.screenX、window.screenY记录下圆相对于屏幕的坐标,将坐标存储进localstorage里,在每个窗口中获取其他窗口的坐标,画出与其的连接线。

此例子只是提供个思路,利用简单的原理实现,多窗口移动时的互动效果。并没有实现像原视频中酷炫的 量子纠缠 效果。

知识点

1.  `window.requestAnimationFrame` 用于定时循环操作,类似于seTimeout。requestAnimationFrame的主要用途是按帧对网页进行重绘,比如动画

2.  `window.screenX, widonw.screenY` 获取浏览器窗口相对于屏幕坐标。

3.  ` window.outerHeight - window.innerHeight  ``outerHeight` 浏览器窗口高度,`innerHeight`浏览器视口高度。两值相减,得到的是除去视口高度之外的浏览器窗口部分的高度。

4.  canvas 画图。

5.  localStorage 本地存储器。基于域(domain)来存储数据的,这意味着同一域下的不同页面可以访问和共享相同的LocalStorage 数据。
  • 画一个圆,并通过requestAnimationFrame把圆的坐标信息实时展示

    画一个圆

    生成一个2d的绘图环境

    draw 绘图出圆形,并将圆心等圆形信息log到页面,且存储进storage里。

  • 给每个窗口的圆定义一个不同的key,并将圆的坐标信息存储进localstroage中

    我们把每个窗口的圆都标记一个唯一的key,并随机生成颜色。

    section1中记录着圆的基本信息和相对于 浏览器窗口 的位置。
    section2中记录着圆相对于 屏幕 的位置信息。

    新增一个窗口,现在,我们看到两个圆都有着自己的key,同时页面log和storage中都存储着两个圆的位置信息。

  • 画出圆与圆之间的连线

    遍历其它窗口存在storage里的position,然后遍历,计算相对自己窗口圆心的横纵间距,最后画线。

  • 覆盖

    视频中可以看到,当两个窗口重叠在一起时,顶层窗口会覆盖里层窗口。

    思路是,把其它圆也画在当前窗口里,也就是说需要在所有窗口里,画出所有圆,只是其它圆坐标会在窗口外面,所以在窗口里看不到而已。

    我们不在每个窗口中只画出自己的圆,而是在遍历的过程中,在当前窗口画出所有的圆。

    我们看下效果,当窗口发生重叠覆盖时,俩圆在最上层窗口同时显示了。

    拖动浏览窗口,圆与圆之间的连线会随着浏览器窗口变化而变化。

    当然,这个例子的核心是浏览器应用在多窗口下进行互相通信 ,上面例子中用的了localStorage,利用的是 localStorage 在同一域下的不同页面可以访问和共享相同的storage 数据。

    浏览在多窗口下进行互相通信的方法也不止一种。

  • Broadcast Channel

    Broadcast Channel 是一个较新的 Web API,用于在不同的浏览器窗口、标签页或框架之间实现跨窗口通信。它基于发布-订阅模式,允许一个窗口发送消息,并由其他窗口接收。

    Broadcast Channel 遵循浏览器的同源策略。这意味着只有在同一个协议、主机和端口下的窗口才能正常进行通信。如果窗口不满足同源策略,将无法互相发送和接收消息。

     <h1>Broadcast Channel 例子</h1>
     <button id="sendMessage">发送消息</button>
     <div id="messages"></div>
    

    在发送和接收消息之前,需要在每个窗口中创建一个 BroadcastChannel 对象,使用相同的频道名称进行初始化。

    const channel = new BroadcastChannel("example-channel");
    

    接收消息:通过监听 BroadcastChannel 对象的 message 事件,可以在窗口中接收到来自其他窗口发送的消息。

    channel.onmessage = (event) => {
            const messages = document.getElementById("messages");
            const messageElement = document.createElement("p");
            messageElement.textContent = event.data;
            messages.appendChild(messageElement);
    };
    

    这里的核心点,是:

    1. 数据向其他 Tab 页面传递的能力

    2. Tab 页面接受其他页面传递过来的数据的能力

    其本质就是一个数据共享池子。

  • SharedWorker API

    SharedWorker API 是 HTML5 中提供的一种多线程解决方案,它可以在多个浏览器 Tab 页面之间共享一个后台线程,从而实现跨页面通信。同一个url 的 worker js 只会创建一个 sharedWorker,其他页面再使用同样的url创建 sharedWorker,会复用已创建的 worker,这个worker由那几个页面共享。

    SharedWorker 需要先建立一个连接池,用于存储与 SharedWorker 建立连接的各个页面的端口对象

    const worker = new SharedWorker("shared-worker.js");
    

    发送消息:通过 SharedWorker 对象的 port.postMessage() 方法,可以向 worker 发送消息。

    worker.port.postMessage(message);
    

    接收消息:通过监听 SharedWorker 对象的 port 的 message 事件,可以在窗口中接收到来自 worker 的消息。

    worker.port.onmessage = (data) => {
        console.log(data)
     };
    

    通过遍历 connections 数组,将消息发送给除当前连接端口对象之外的所有连接。这样,消息就可以在不同的浏览器 Tab 页面之间传递

    const connections = [];
    onconnect = function (event) {
      var port = event.ports[0];
      connections.push(port);
      port.onmessage = function (event) {
        // 接收到消息时,向所有连接发送该消息
        connections.forEach(function (conn) {
          if (conn !== port) {
            conn.postMessage(event.data);
          }
        });
      };
      port.start();
    };
    

    ​​​

  • 对比

    通信范围:BroadcastChannel 可以在同一个浏览器中的不同标签页之间进行消息通信,而 SharedWorker 可以在不同的标签页之间共享执行环境,也可以在不同的窗口或浏览器中进行通信。

    数据广播:BroadcastChannel 只能在同一个浏览器中的标签页之间广播消息,而 SharedWorker 可以将消息传递给所有连接到同一个 SharedWorker 的窗口或标签页。

    共享数据:SharedWorker 允许在每个连接的标签页之间共享数据,标签页可以通过 SharedWorker 共享同一个状态、变量或对象,这可以用于共享状态或实现共享功能。而 BroadcastChannel 只是用于传递简单的消息,不具有共享数据的能力。

    生命周期:SharedWorker 是一个在后台运行的线程,会在没有连接的标签页时继续存在,而 BroadcastChannel 只在有连接的标签页时才有效。

    BroadcastChannel 更适合于在同一个浏览器的不同标签页之间进行简单的消息通信,而 SharedWorker 则更适合共享状态、数据和功能,可以实现更复杂的跨标签页或跨窗口的通信需求。

    兼容性对比:

    在兼容性的方面,localStorage是支持性最好的,抛开浏览器支持性来说,BroadcastChannel是个不错的解决方案。即时通信、随时关闭频道、不会产生因为localStorage不及时清理而引起的问题。

    在查找资料时还发现一个第三方库:​https://github.com/pubkey/broadcast-channel​ 能够根据使用的环境和浏览器版本,会自动选择合适方法,支持BroadcastChannel、indexDB、localStorage三种方式。

  • 最后,上面提到的不论是localstorage、SharedWorker、BroadcastChannel,都是不支持跨域的,如果要支持跨域,一般需要借助后端实现通信,常见的有WebSocket、Server-Sent Events (SSE)

  • 完整代码

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Canvas Circle</title>
      </head>
      <body style="height: 100vh">
        <section id="section1" style="white-space: pre-wrap"></section>
        <section id="section2" style="white-space: pre-wrap"></section>
        <canvas
          id="canvas"
          style="position: absolute; left: 0; top: 0; border: 1px solid red"
        ></canvas>
      </body>
      <script>
        const canvas = document.getElementById("canvas");
        const ctx = canvas.getContext("2d");
        const section1 = document.getElementById("section1");
        const section2 = document.getElementById("section2");
    
        const key = Date.now();
        const color = ["red", "yellow", "blue"][key % 3];
    
        window.onunload = function () {
          localStorage.removeItem(key);
        };
    
        function draw() {
          const { clientWidth, clientHeight } = document.body;  // 获取body高宽
          const { screenX, screenY } = window;  // // 获取浏览器相对屏幕坐标
          const barHeight = window.outerHeight - window.innerHeight;  // 获取浏览器body顶部地址栏高度
    
          // 设置canvas为整个body高宽,铺满body
          canvas.width = clientWidth;
          canvas.height = clientHeight;
    
          // 圆心相对于浏览器窗口坐标
          const x = clientWidth / 2;
          const y = clientHeight / 2;
    
          // 圆心相对于屏幕坐标
          let positons = {
            top: y + barHeight + screenY,
            left: x + screenX,
            color,
          };
    
          localStorage.setItem(key, JSON.stringify(positons));
    
          Object.keys(window.localStorage).forEach((key) => {
            const positons2 = JSON.parse(localStorage.getItem(key));
            const w = positons2.left - positons.left;
            const h = positons2.top - positons.top;
    
            // 画出所有的圆
            ctx.fillStyle = positons2.color;
            ctx.beginPath();
            ctx.arc(x + w, y + h, 15, 0, Math.PI * 2);
            ctx.fill();
    
            // 对于每个圆,画出连接当前圆的线
            Object.keys(window.localStorage).forEach((otherKey) => {
              if (key !== otherKey) {
                const otherPositions = JSON.parse(localStorage.getItem(otherKey));
                const otherW = otherPositions.left - positons.left;
                const otherH = otherPositions.top - positons.top;
    
                // 画线连接两个圆
                ctx.beginPath();
                ctx.moveTo(x + w, y + h);
                ctx.lineTo(x + otherW, y + otherH);
                ctx.stroke();
              }
            });
          });
    
          section1.textContent = JSON.stringify(
            { clientWidth, clientHeight, barHeight, key, color },
            "",
            2
          );
    
          section2.textContent = JSON.stringify(positons, "", 2);
    
          window.requestAnimationFrame(draw);
        }
        window.requestAnimationFrame(draw);
      </script>
    </html>