最近,社交網路上出現了乙個 gif **,展示了 Bjorn Staal 製作的令人驚嘆的藝術作品。
我想重現它,但缺乏球體、粒子和物理學方面的 3D 技能,我的目標是了解如何讓乙個視窗對另乙個視窗的位置做出反應。
從本質上講,在多個視窗之間共享狀態,我發現這是 bjorn 專案最酷的方面之一!
由於我找不到關於該主題的好文章或教程,我決定與您分享我的發現。
讓我們嘗試基於 BJORN 的工作建立乙個簡化的概念驗證 (PoC)!
我做的第一件事是列出我所知道的在多個客戶之間共享資訊的所有方式:
顯然,擁有乙個伺服器(帶有輪詢或 websocket)可以簡化問題。 但是,這是不可能的,因為 bjorn 在不使用伺服器的情況下取得了成果。
本地儲存本質上是瀏覽器鍵值儲存,通常用於在瀏覽器會話之間儲存資訊。 雖然通常用於儲存身份驗證令牌或重定向 URL,但它可以儲存任何可序列化的內容。 您可以在此處了解更多資訊。
我最近發現了一些有趣的本地儲存 API,包括每當本地儲存被同一**的另乙個會話更改時觸發的儲存事件。
我們可以通過將每個視窗的狀態儲存在本地儲存中來利用這一點。 每當乙個視窗更改其狀態時,其他視窗都會通過儲存事件進行更新。
這是我最初的想法,這似乎是 bjorn 選擇的解決方案,因為他在這裡分享了他的本地儲存管理器**以及將其與 threejs 一起使用的示例。
但當我發現有**可以解決這個問題時,我想看看有沒有別的辦法......劇透警告:是的,有!
在這個華麗的術語背後是乙個引人入勝的概念——網路工作者的概念。
簡單來說,工作執行緒本質上是在另乙個執行緒上執行的第二個指令碼。 雖然它們無法訪問 DOM(因為它們存在於 HTML 文件之外),但它們仍然可以與您的主指令碼進行通訊。
它們主要用於通過處理後台作業(例如預取資訊)或處理不太重要的任務(例如流日誌和輪詢)來解除安裝主指令碼。
共享工作執行緒是一種特殊型別的網路工作執行緒,可以與同一指令碼的多個例項進行通訊,這使得它們對我們的用例很有意義!好吧,讓我們直接跳進去吧!
如前所述,worker 是“第二個指令碼”,有自己的切入點。 根據您的設定(TypeScript、程式、Dev Server),您可能需要調整 tsconfig、新增指令或使用特定的匯入語法。
我無法涵蓋使用 Web Worker 的所有可能方法,但您可以在 mdn 或網際網絡上找到資訊。
如果需要,我很樂意為本文寫前傳,詳細說明設定它們的所有方法!
就我而言,我使用 vite 和打字稿,所以我需要乙個工人ts 檔案並將其安裝@types sharedworker 作為開發依賴項。 我們可以使用以下語法在主指令碼中建立連線:
new sharedworker(new url("worker.ts", import.meta.url));
基本上,我們需要:
識別每個視窗會跟蹤所有視窗狀態,一旦乙個視窗改變狀態,提醒其他視窗重繪我們的狀態將非常簡單:
type windowstate = ;
當然,最重要的資訊是視窗screenxthemwindow。Screeny 告訴我們視窗相對於顯示器左上角的位置。
我們將有兩種型別的訊息:
每當每個視窗的狀態發生更改時,都會發布乙個包含其新狀態的 WindowStateChangedMessage。 工作人員將向所有其他視窗傳送更新,以提醒他們其中乙個視窗已更改。 工作執行緒將傳送包含所有視窗狀態的 syncmessage。 我們可以從乙個普通工人開始,他看起來有點像這樣:
// worker.ts let windows: [= onconnect = ()=> ;
我們與 sharedworker 的基本連線如下所示。 我有一些基本函式可以生成 id 並計算當前視窗狀態,並且我還有一些輸入到一種訊息型別中,我們可以將其稱為 workermessage:
// main.ts import from "./types"; import from "./windowstate"; const sharedworker = new sharedworker(new url("worker.ts", import.meta.url));let currentwindow = getcurrentwindowstate();let id = generateid();
一旦我們啟動應用程式,我們應該提醒工作人員有乙個新視窗,所以我們立即傳送一條訊息:
// main.ts sharedworker.port.postmessage(, satisfies workermessage);
我們可以在工作端監聽此訊息並相應地更改 onmessage。 基本上,一旦工作執行緒收到 WindowStateChanged 訊息,它要麼是乙個新視窗,我們將其附加到狀態,要麼是乙個已更改的舊視窗。 那麼我們應該提醒大家,狀態已經改變了:
// worker.ts port.onmessage = function (event: messageevent) = msg.payload; const oldwindowindex = windows.findindex((w) => w.id === id); if (oldwindowindex !== -1) else );windows.foreach((w) => // send sync here );break; }
為了傳送同步,我實際上需要一些技巧,因為“port”屬性無法序列化,所以我將其字串化並解析回來。 因為我很懶,所以我不只是將視窗對映到更可序列化的陣列:
w.port.postmessage(, satisfies workermessage);
現在是時候畫點東西了!
當然,我們不做複雜的 3D 球體:我們只會在每個視窗的中心畫乙個圓圈,並在球體之間畫一條線!
我將使用 HTML Canvas 的基本 2D 上下文進行繪製,但您可以使用任何您想要的東西。 畫乙個圓圈,很簡單:
const drawcentercircle = (ctx: canvasrenderingcontext2d, center: coordinates) => = 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 = (:=> ;const currentwindowcoordinate = ; return currentwindowcoordinate; }
如您所知,現在我們在同乙個相對坐標系中有兩個點,現在可以畫線了!
const drawconnectingline = (:=> ;const targetwindowoffset: coordinates = ; const origin = getwindowcenter(hostwindow); const target = getwindowcenter(targetwindow); const targetwithbasechange = basechange();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) => )=> )
作為最後一步,我們只需要定期檢查視窗是否發生變化,如果發生變化,則傳送訊息。
setinterval(()=> )satisfies workermessage); currentwindow = newwindow; }100);
您可以在此儲存庫中找到完整的 **。 實際上,我已經做了很多實驗來使它更抽象,但要點是一樣的。
如果您在多個視窗上執行它,希望您得到與此相同的結果!