【问题标题】:JavaScript: How to know if a connection with a shared worker is still alive?JavaScript:如何知道与共享工作者的连接是否仍然存在?
【发布时间】:2012-11-19 16:04:22
【问题描述】:

我正在尝试使用共享工作者来维护 Web 应用程序的所有窗口/选项卡的列表。因此使用以下代码:

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

每次创建窗口时,都会与shared-worker.js worker 建立连接,worker 会将与窗口的连接添加到connections 列表中。

当用户关闭一个窗口时,它与共享工作者的连接会过期,应该从connections 变量中删除。但我没有找到任何可靠的方法来做到这一点。

查看specificationconnections 变量的对象似乎没有用于检查连接是否仍然存在的属性/函数。

有可能吗?
同样,总体目标是获得所有窗口/选项卡的列表。

编辑:一种方法是让共享工作者向窗口发送消息并期待回复。如果共享工作者没有收到回复,那么它将假定窗口已关闭。在我的实验中,这种方法并不可靠。问题是无法判断窗口是关闭还是需要很长时间才能回复。

【问题讨论】:

  • MessagePorts 没有onclose 处理程序吗?让我在规范中检查一下。
  • 貌似有:doc.wakanda.org/SharedWorker.301-688965.en.html 有“断开连接”的情况。
  • 也许你可以 ping 连接,假设每 2 秒?
  • @dumbmatter "每次创建窗口时都会与shared-worker.js worker 建立连接" window 是如何创建的?

标签: javascript html web-applications webkit web-worker


【解决方案1】:

这仅与 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;
    }
}

【讨论】:

  • 请注意,这在选项卡突然结束的情况下不起作用。例如。如果标签崩溃或用户结束标签的进程。
  • 您不应该为此使用“卸载”事件,而不是“之前卸载”吗?拥有两者的全部意义在于,“beforeunload”处理程序可以防止卸载发生(如果它是由于导航而发生的)。因此,如果您的处理程序在另一个阻止页面关闭的处理程序之前运行,您最终会得到一个打开的页面,该页面已被错误地从共享工作人员的连接列表中删除!
【解决方案2】:

我整个星期都在研究文档,解决同一个问题。

问题在于 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 来保持连接性知识。可以做到,但后面会很痛。

【讨论】:

  • 很高兴我不是唯一一个在寻找解决方案的人。我也尝试了“ping-message”方法,但它似乎并不可靠;无法判断选项卡是否已关闭并因此不响应,或者此选项卡是否需要很长时间才能响应。
  • 是的,我也看过规范。似乎他们已经删除了close 事件,至少有一个back in 2009
  • 我认为目前最好的方法就是有一个自动关闭的双向回调,除非选项卡/工作人员响应。假设您有使用 SharedWorker 的任何备份实现。不理想,但实用。
  • “这也意味着如果 SharedWorker 进程死亡......”。 @CDeLorme,我真的不明白。在什么情况下 SharedWorker 进程会死掉,而任何仍然持有对它的引用的窗口仍然活着?你可以通过(比如)Windows 任务管理器杀死 SharedWorker 线程,而不杀死浏览器窗口本身吗?
【解决方案3】:

PortCollection 会派上用场,但似乎没有在任何浏览器中实现。

它充当 MessagePort 对象的不透明数组,因此允许在对象不再相关时进行垃圾收集,同时仍允许脚本遍历 MessagePort 对象。

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

编辑;刚刚为 Chrome 提出了一个问题; http://crbug.com/263356

【讨论】:

  • 它的作用类似于 WeakValueMap
  • 规范放弃了它。
【解决方案4】:

...如何使用您在编辑中建议的方法,即使用保持活动 ping,但是:

就在关闭任何无响应的连接之前,通过它发送“请重新连接”消息,这样如果窗口没有真正关闭,只是忙,它会知道它必须重新连接?强>

根据@Adria 的解决方案,这种技术可能应该与从窗口 onunload 事件中发送明确的“我现在正在关闭”消息相结合,以便有效地处理正常的窗口终止并且没有任何延迟。

这仍然有点不可靠,因为非常繁忙的窗口可能会暂时从 SharedWorker 的列表中删除,然后再重新连接......但实际上我看不出你能做得更好:考虑一下,如果一个窗口挂起,实际上说这与它在无限长的时间内“忙碌”是没有区别的,所以你不能真正抓住一个而不抓住另一个(无论如何,在任何有限的时间内)。

根据您的应用程序,让非常繁忙的窗口暂时被除名可能是一个大问题,也可能不是一个大问题。

注意,保持活动的 ping 应该从 SharedWorker 发送到 windows,然后应该响应:如果你尝试在 windows 中简单地使用 setTimout(),你会遇到 setTimeout() on 背景的问题 窗口可以延迟很长时间(我相信在当前浏览器上最多 1 秒),而 SharedWorker 的 setTimeout()s 应该按计划运行(给或需要几毫秒),空闲的背景窗口将唤醒并响应立即发布 SharedWorker 消息。


这里有一个关于这种技术的简洁演示:

  1. 为每个窗口分配一个唯一的数字 ID
  2. 跟踪单个“活动”窗口
  3. 跟踪当前窗口 ID 列表和总数
  4. 始终让所有窗口了解上述所有情况

sharedworker.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")});
  }
}

sharedworker.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;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-10
    • 2022-01-15
    • 2015-05-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多