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

时间:2012-12-01 17:56:30

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

我正在尝试使用共享工作者来维护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工作者建立连接,并且工作人员将窗口的连接添加到connections列表。

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

查看specification connections变量的对象似乎没有包含属性/函数来检查连接是否仍然存在。

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

编辑:一种方法是将共享工作人员的消息发送到Windows并期待回复。如果共享工作者没有收到回复,那么它将认为该窗口已关闭。在我的实验中,这种方法并不可靠;问题是没有办法判断一个窗口是关闭还是只是花了很长时间才能回复。

4 个答案:

答案 0 :(得分:13)

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

答案 1 :(得分:9)

我整个星期都在处理同样问题的文档。

问题是MessagePort规范。坏消息是它没有错误处理,也没有标志,方法或事件来确定它是否已被关闭。

好消息是我已经创建了一个可行的解决方案,但这是很多代码。

请记住,即使在支持的浏览器中,活动的处理方式也不同。例如,如果您尝试发送消息或关闭已关闭的端口,Opera将抛出错误。坏消息是您必须使用try-catch来处理错误,好消息是您可以使用该反馈来关闭至少一侧的端口。

Chrome和Safari无声地失败,不会给您任何反馈,也无法终止无效对象。


我的解决方案涉及交付确认或自定义“回调”方法。您使用setTimeout并使用您的命令将其ID传递给SharedWorker,在处理命令之前,它会发回确认以取消超时。该超时通常挂钩到closeConnection()方法。

这需要采用被动方法而不是先发制人,最初我玩弄了TCP / IP协议模型但涉及创建更多功能来处理每个进程。


一些Psuedo-Code作为例子:

客户端/标签代码:

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,从而允许它们使用每个tab-for-theirself的回退方法。使用相同代码的所有其他浏览器固有的。


因此,正如您可以看到用于传递确认的回调组合,必须从两端使用setTimeout和setInterval来维护连接知识。它可以做到,但它是后方巨大的痛苦。

答案 2 :(得分:1)

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

  

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

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

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

答案 3 :(得分:0)

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

只是在关闭任何无响应的连接之前,通过它发送“请重新连接”消息,以便如果一个窗口并没有真正关闭,只是很忙,它会知道它必须重新连接吗?

按照@Adria的解决方案,该技术可能应该与从窗口onunload事件发送显式的“我正在关闭”消息一起使用,以使正常的窗口终止得到有效处理而没有任何延迟。

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

根据您的应用程序,将非常繁忙的窗口暂时从列表中删除可能不是大问题。

请注意,保持活动的ping操作应从SharedWorker发送到Windows,然后应响应:如果您仅在Windows中尝试使用setTimout(),则会遇到背景上的setTimeout()问题窗口可能会延迟很长时间(我相信在当前的浏览器上可能会延迟1秒),而SharedWorker的setTimeout()应该按计划运行(给定或花费几毫秒),并且空闲的背景窗口会唤醒并做出响应立即发布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;