33

我正在尝试使用共享工作者来维护 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变量中删除。但我没有找到任何可靠的方法来做到这一点。

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

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

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

4

4 回答 4

19

这仅与 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;
    }
}
于 2014-10-10T05:14:13.840 回答
13

我整个星期都在文档中深入研究同一个问题。

问题是 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 以保持连接的知识。它可以做到,但它是一个巨大的痛苦在后方。

于 2012-12-03T07:11:23.030 回答
1

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

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

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

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

于 2012-12-01T18:36:16.323 回答
0

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

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

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

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

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

请注意,保持活动的 ping 应从 SharedWorker 发送到 windows,然后应响应:如果您尝试在 windows 中简单地使用 setTimout(),您会遇到后台windows 上的 setTimeout() 可能会延迟很长时间的问题(我相信在当前浏览器上最多 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;
于 2020-07-23T14:23:27.360 回答