簡體   English   中英

JavaScript:如何知道與共享工作者的連接是否仍然存在?

[英]JavaScript: How to know if a connection with a shared worker is still alive?

我正在嘗試使用共享工作者來維護 web 應用程序的所有窗口/選項卡的列表。 因此使用以下代碼:

//lives in shared-worker.js
var connections=[];//this represents the list of all windows/tabs
onconnect=function(e){
  connections.push(e.ports[0]);
};

每次創建 window 時,都會與shared-worker.js worker 建立連接,並且 worker 會將與 window 的連接添加到connections列表中。

當用戶關閉 window 時,它與共享 worker 的連接到期,應該從connections變量中刪除。 但是我找不到任何可靠的方法來做到這一點。

查看規范connections變量的對象似乎沒有屬性/函數來檢查連接是否仍然存在。

是否可以?
同樣,總體目標是擁有所有窗口/選項卡的列表。

編輯:一種方法是讓共享工作人員發送消息 windows 並期待回復。 如果共享工作者沒有收到回復,那么它會認為 window 已關閉。 在我的實驗中,這種方法並不可靠; 問題是無法判斷 window 是否已關閉或只是需要很長時間才能回復。

這僅與 beforeunload 一樣可靠,但似乎有效(在 Firefox 和 Chrome 中測試)。 我絕對喜歡它而不是投票解決方案。

// Tell the SharedWorker we're closing
addEventListener( 'beforeunload', function()
{
    port.postMessage( {command:'closing'} );
});

然后在 SharedWorker 中處理端口對象的清理工作。

e.ports[0].onmessage = function( e )
{
    const port = this,
    data = e.data;

    switch( data.command )
    {
        // Tab closed, remove port
        case 'closing': myConnections.splice( myConnections.indexOf( port ), 1 );
            break;
    }
}

我整個星期都在文檔中深入研究相同的問題。

問題是 MessagePort 規范。 壞消息是它沒有錯誤處理,也沒有標志、方法或事件來確定它是否已關閉。

好消息是我已經創建了一個可行的解決方案,但它有很多代碼。

請記住,即使在支持的瀏覽器中,活動的處理方式也不同。 例如,如果您嘗試發送消息或關閉關閉的端口,Opera 將拋出錯誤。 壞消息是您必須使用 try-catch 來處理錯誤,好消息是您可以使用該反饋至少在一側關閉端口。

Chrome 和 Safari 無聲無息地失敗,不會給您任何反饋,也無法結束無效對象。


我的解決方案涉及交付確認或自定義“回調”方法。 您使用 setTimeout 並將其 ID 與您的命令一起傳遞給 SharedWorker,然后在處理該命令之前,它會發送回確認以取消超時。 該超時通常與 closeConnection() 方法掛鈎。

這需要一種反應式方法而不是先發制人,最初我玩弄使用 TCP/IP 協議模型,但這涉及創建更多函數來處理每個進程。


一些偽代碼作為例子:

客戶端/標簽代碼:

function customClose() {
    try {
        worker.port.close();
    } catch (err) { /* For Opera */ }
}
function send() {
    try {
        worker.port.postMessage({command: "doSomething", content: "some Data", id: setTimeout(function() { customClose(); ); }, 1000);
    } catch (err) { /* For Opera */ }
}

線程/工人代碼:

function respond(p, d) {
    p.postMessage({ command: "confirmation", id: d.id });
}
function message(e) {// Attached to all ports onmessage
    if (e.data.id) respond(this, e.data);
    if (e.data.command) e.data.command(p, e.data);// Execute command if it exists passing context and content
}

我在這里放了一個完整的演示: http : //www.cdelorme.com/SharedWorker/

我是堆棧溢出的新手,所以我不熟悉他們如何處理大型代碼帖子,但我的完整解決方案是兩個 150 行的文件。


僅使用交付確認並不完美,因此我通過添加其他組件來改進它。

特別是我正在為 ChatBox 系統調查這個,所以我想使用 EventSource (SSE)、XHR 和 WebSockets,據說 SharedWorker 對象內部只支持 XHR,如果我想讓 SharedWorker 做所有服務器,這會產生限制溝通。

另外,因為它需要在沒有 SharedWorker 支持的瀏覽器上工作,所以我將在 SharedWorker 內部創建長期重復處理,這沒有多大意義。

所以最后,如果我實現 SharedWorker,它將僅作為打開選項卡的通信渠道,一個選項卡將是控制選項卡。

如果控制選項卡關閉,SharedWorker 將不知道,因此我向 SharedWorker 添加了一個 setInterval 以每隔幾秒鍾向所有打開的端口發送一個空響應請求。 這允許 Chrome 和 Safari 在沒有處理任何消息時消除關閉的連接,並允許更改控制選項卡。

但是,這也意味着,如果 SharedWorker 進程終止,則選項卡必須有一個時間間隔以每隔一段時間使用相同的方法與 SharedWorker 簽入,從而允許它們使用每個選項卡為主題所固有的回退方法其他瀏覽器使用相同的代碼。


因此,正如您所看到的用於交付確認的回調組合,必須從兩端使用 setTimeout 和 setInterval 來維護連接知識。 這是可以做到的,但這是一個巨大的背部疼痛。

所以實際上有一種方法可以計算出哪些端口仍然存在,哪些不存在。 成功的關鍵是WeakRef和 HTML 標准的“端口和垃圾收集”部分,它引用了垃圾收集如何與MessagePort一起工作。

完整代碼在這里: https://gist.github.com/Akxe/b4cfefa0086f9a995a3578818af63ad9

class 旨在可擴展以適應大多數用例。

PortAwareSharedWorker

PortAwareSharedWorker是一個 singleton,它檢查每個連接端口的alive (更多信息見下文)狀態。 它還公開了包含所有當前“活動”端口的getOpenPorts方法。

PortAwareSharedWorkerPort作為MessagePort的替代品

MessagePort永遠不會暴露給用戶。 相反,具有弱引用的包裝器是。 它通過每個動作檢查其引用的有效性,並處理某些情況。

注意事項

該解決方案不是防彈的。 即使端口應該關閉(沒有辦法繞過它),弱引用也是可解析的。 為了緩解這種情況,提供了PortAwareSharedWorkerPort包裝器,它將處理諸如調用postMessage到一個已經關閉的端口的事情,因為它在某些瀏覽器中崩潰了。

此外,最值得注意的是,規范規定如下:

此外,MessagePort object 在任務隊列中存在任務引用的事件時不得對 MessagePort object 進行垃圾回收,該事件將在 MessagePort object 上分派,或者 MessagePort 對象的端口消息隊列已啟用且不為空。

我不是 100% 確定引用的含義,但我的結論是,如果開發人員在onmessage回調中創建了一個長時間運行的任務,該任務會導致與端口的進一步交互,則無法關閉端口。 例如:消息進來,發出一個獲取請求,並在它返回后發回一個響應。 在請求/處理提取期間,無法收集端口。

如果我錯了請糾正我,但我認為將消息端口包裝在WeakRef中足以使其再次可收集。

PortCollection會派上用場,但似乎沒有在任何瀏覽器中實現。

它充當 MessagePort 對象的不透明數組,因此允許對象在停止相關時被垃圾收集,同時仍然允許腳本遍歷 MessagePort 對象。

來源; http://www.whatwg.org/specs/web-apps/current-work/multipage/web-messaging.html#portcollection

編輯; 剛剛為 Chrome 提出了一個問題; http://crbug.com/263356

...如何使用您在編輯中建議的方法,即使用保持活動的 ping,但是:

就在關閉任何無響應的連接之前,通過它發送“請重新連接”消息,這樣如果一個窗口沒有真正關閉,只是忙,它就會知道它必須重新連接?

根據@Adria 的解決方案,此技術可能應該與從窗口 onunload 事件發送顯式“我正在關閉”消息相結合,以便有效地處理正常的窗口終止並且沒有任何延遲。

這仍然有些不可靠,因為非常繁忙的窗口可能會暫時從 SharedWorker 的列表中刪除,然后再重新連接...這與無限期地“忙碌”一段時間沒有區別,所以你不能真正抓住一個而不抓住另一個(無論如何,在任何有限的時間內)。

根據您的應用程序,暫時將非常繁忙的窗口從列表中刪除可能是也可能不是大問題。

請注意,保持活動的 ping 應該從 SharedWorker 發送到窗口,然后窗口應該響應:如果您嘗試在窗口中簡單地使用 setTimout(),您會遇到后台窗口上的 setTimeout() 可能會延遲很長時間的問題(我相信在當前瀏覽器上最多 1 秒),而 SharedWorker 的 setTimeout()s 應該按計划運行(給予或花費幾毫秒),並且空閑的后台窗口將被喚醒並立即響應發布的 SharedWorker 消息。


這是這種技術的一個簡潔的小演示,即:

  1. 為每個窗口分配一個唯一的數字 ID
  2. 跟蹤單個“活動”窗口
  3. 跟蹤當前窗口 ID 的列表和總數
  4. 隨時讓所有窗口了解以上所有信息

共享工作者.html

<!doctype html>
<head>
  <title>Shared Worker Test</title>
  <script type="text/javascript" src="sharedworker-host.js" async></script>
  <script>
    function windowConnected(init){ if (init) { document.title = "#"+thisWindowID; document.getElementById("idSpan").textContent = thisWindowID; } document.body.style.backgroundColor = "lightgreen"; }
    function windowDisconnected(){ document.title = "#"+thisWindowID; document.body.style.backgroundColor = "grey"; }
    function activeWindowChanged(){ document.getElementById("activeSpan").textContent = activeWindowID; document.title = "#"+thisWindowID+(windowIsActive?" [ACTIVE]":""); document.body.style.backgroundColor = (windowIsActive?"pink":"lightgreen"); }
    function windowCountChanged(){ document.getElementById("countSpan").textContent = windowCount; }
    function windowListChanged(){ document.getElementById("listSpan").textContent = otherWindowIDList.join(", "); }
    function setActiveClick(){ if (setWindowActive) setWindowActive(); }
    function longOperationClick(){ var s = "", start = Date.now(); while (Date.now()<(start+10000)) { s += Math.sin(Math.random()*9999999).toString; s = s.substring(s.length>>>1); } return !!s; }
    window.addEventListener("unload",function(){window.isUnloading = true});
    window.addEventListener("DOMContentLoaded",function(){window.DOMContentLoadedDone = true});
  </script>
  <style>
    body {padding:40px}
    span {padding-left:40px;color:darkblue}
    input {margin:100px 60px}
  </style>
</head>
<body>
   This Window's ID: <span id="idSpan">???</span><br><br>
   Active Window ID: <span id="activeSpan">???</span><br><br>
   Window Count: <span id="countSpan">???</span><br><br>
   Other Window IDs: <span id="listSpan">???</span><br><br>
   <div>
     <input type="button" value="Set This Window Active" onclick="setActiveClick()">
     <input type="button" value="Perform 10-second blocking computation" onclick="longOperationClick()">
   </div>
</body>
</html>

sharedworker-host.js

{ // this block is just to trap 'let' variables inside
  let port = (new SharedWorker("sharedworker.js")).port;
  var thisWindowID = 0, activeWindowID = 0, windowIsConnected = false, windowIsActive = false, windowCount = 0, otherWindowIDList = [];

  //function windowConnected(){}         //
  //function windowDisconnected(){}      //
  //function activeWindowChanged(){}     // do something when changes happen... these need to be implemented in another file (e.g. in the html in an inline <script> tag)
  //function windowCountChanged(){}      //
  //function windowListChanged(){}       //

  function setWindowActive() { if (thisWindowID) port.postMessage("setActive"); }
  function sortedArrayInsert(arr,val) { var a = 0, b = arr.length, m, v; if (!b) arr.push(val); else { while (a<b) if (arr[m = ((a+b)>>>1)]<val) a = m+1; else b = m; if (arr[a]!==val) arr.splice(a,0,val); }}
  function sortedArrayDelete(arr,val) { var a = 0, b = arr.length, m, v; if (b) { while (a<b) if (arr[m = ((a+b)>>>1)]<val) a = m+1; else b = m; if (arr[a]===val) arr.splice(a,1); }}

  let msgHandler = function(e)
  {
    var data = e.data, msg = data[0];
    if (!(windowIsConnected||(msg==="setID")||(msg==="disconnected"))) { windowIsConnected = true; windowConnected(false); }
    switch (msg)
    {
      case "ping": port.postMessage("pong"); break;
      case "setID": thisWindowID = data[1]; windowConnected(windowIsConnected = true); break;
      case "setActive": if (activeWindowID!==(activeWindowID = data[1])) { windowIsActive = (thisWindowID===activeWindowID); activeWindowChanged(); } break;
      case "disconnected": port.postMessage("pong"); windowIsConnected = windowIsActive = false; if (thisWindowID===activeWindowID) { activeWindowID = 0; activeWindowChanged(); } windowDisconnected(); break;
    // THE REST ARE OPTIONAL:
      case "windowCount": if (windowCount!==(windowCount = data[1])) windowCountChanged(); break;
      case "existing": otherWindowIDList = data[1].sort((a,b) => a-b); windowListChanged(); break;
      case "opened": sortedArrayInsert(otherWindowIDList,data[1]); windowListChanged(); break;
      case "closed": sortedArrayDelete(otherWindowIDList,data[1]); windowListChanged(); break;
    }
  };

  if (!window.isUnloading)
  {
    if (window.DOMContentLoadedDone) port.onmessage = msgHandler; else window.addEventListener("DOMContentLoaded",function(){port.onmessage = msgHandler});
    window.addEventListener("unload",function(){port.postMessage("close")});
  }
}

共享工作者.js

// This shared worker:
// (a) Provides each window with a unique ID (note that this can change if a window reconnects due to an inactivity timeout)
// (b) Maintains a list and a count of open windows
// (c) Maintains a single "active" window, and keeps all connected windows apprised of which window that is
//
// It needs to RECEIVE simple string-only messages:
//   "close" - when a window is closing
//   "setActive" - when a window wants to be set to be the active window
//   "pong" (or in fact ANY message at all other than "close") - must be received as a reply to ["ping"], or within (2 x pingTimeout) milliseconds of the last recived message, or the window will be considered closed/crashed/hung
//
// It will SEND messages:
//   ["setID",<unique window ID>] - when a window connects, it will receive it's own unique ID via this message (this must be remembered by the window)
//   ["setActive",<active window ID>] - when a window connects or reconnects, or whenever the active window changes,  it will receive the ID of the "active" window via this message (it can compare to it's own ID to tell if it's the active window)
//   ["ping"] - a window sent this message should send back a "pong" message (or actually ANY message except "close") to confirm it's still alive
//   ["disconnected"] - when a window is disconnected due to a ping timeout, it'll recieve this message; assuming it hasn't closed it should immediately send a "pong", in order to reconnect.
// AND OPTIONALLY (REMOVE lines noted in comments to disable):
// IF EACH WINDOW NEEDS (ONLY) A RUNNING COUNT OF TOTAL CONNECTED WINDOWS:
//   ["windowCount",<count of connected windows>] - sent to a window on connection or reconnection, and whenever the window count changes
// OR ALTERNATIVELY, IF EACH WINDOW NEEDS A COMPLETE LIST OF THE IDs OF ALL OTHER WINDOWS:
//   ["existing",<array of existing window IDs>] - sent upon connectionor reconnection
//   ["opened",<ID of just-opened window>] - sent to all OTHER windows, when a window connects or reconnects
//   ["closed",<ID of closing window>] - sent to all OTHER windows, when a window disconnects (either because it explicitly sent a "close" message, or because it's been too long since its last message (> pingTimeout))

const pingTimeout = 1000;  // milliseconds
var count = 0, lastID = 0, activeID = 0, allPorts = {};

function handleMessage(e)
{
  var port = this, msg = e.data;
  if (port.pingTimeoutID) { clearTimeout(port.pingTimeoutID); port.pingTimeoutID = 0; }
  if (msg==="close") portClosed(port,false); else
  {
    if (!allPorts[port.uniqueID]) connectPort(port,false);  // reconnect disconnected port
    if (msg==="setActive") setActive(port.uniqueID);
    port.pingTimeoutID = setTimeout(function(){pingPort(port)},pingTimeout);
  }
}

function setActive(portID)  // if portID is 0, this actually sets the active port ID to the first port in allPorts{} if present (or 0 if no ports are connected)
{
  if (activeID!==portID)
  {
    activeID = portID;
    for(var pID in allPorts) if (allPorts.hasOwnProperty(pID)) allPorts[pID].postMessage(["setActive",(activeID||(activeID = +pID))]);
  }
}

function pingPort(port)
{
  port.postMessage(["ping"]);
  port.pingTimeoutID = setTimeout(function(){portClosed(port,true)},pingTimeout);
}

function portClosed(port,fromTimeout)
{
  var portID = port.uniqueID;
  if (fromTimeout) port.postMessage(["disconnected"]); else { clearTimeout(port.pingTimeoutID); port.close(); }
  port.pingTimeoutID = 0;
  if (allPorts[portID])
  {
    delete allPorts[portID];
    --count;
    if (activeID===portID) setActive(0);
    for(var pID in allPorts) if (allPorts.hasOwnProperty(pID)) allPorts[pID].postMessage(["closed",portID]);  // REMOVE if windows don't need a list of all other window IDs
    for(var pID in allPorts) if (allPorts.hasOwnProperty(pID)) allPorts[pID].postMessage(["windowCount",count]);  // REMOVE if change of window-count doesn't need to be broadcast to all windows
  }
}

function newConnection(e)
{
  var port = e.source;
  port.uniqueID = ++lastID;
  port.onmessage = handleMessage;
  connectPort(port,true);
}

function connectPort(port,initialConnection)
{
  var portID = port.uniqueID;
  port.postMessage(["existing",Object.keys(allPorts).map(x => +x)]);for(var pID in allPorts) if (allPorts.hasOwnProperty(pID)) allPorts[pID].postMessage(["opened",portID]);  // REMOVE if windows don't need a list of all other window IDs
  allPorts[portID] = port;
  ++count;
  for(var pID in allPorts) if (allPorts.hasOwnProperty(pID)) allPorts[pID].postMessage(["windowCount",count]);  // REMOVE if change of window-count doesn't need to be broadcast to all windows
  if (initialConnection) { port.postMessage(["setID",lastID]); port.pingTimeoutID = setTimeout(function(){pingPort(port)},pingTimeout); }
  if (!activeID) setActive(portID); else port.postMessage(["setActive",activeID]);
}

onconnect = newConnection;

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM