简体   繁体   English

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

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

I'm trying to use a shared worker to maintain a list of all the windows/tabs of a web application.我正在尝试使用共享工作者来维护 web 应用程序的所有窗口/选项卡的列表。 Therefore following code is used:因此使用以下代码:

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

Everytime a window is created a connection is established with the shared-worker.js worker and the worker adds the connection with the window to the connections list.每次创建 window 时,都会与shared-worker.js worker 建立连接,并且 worker 会将与 window 的连接添加到connections列表中。

When a user closes a window its connection with the shared worker expires and should be removed from the connections variable.当用户关闭 window 时,它与共享 worker 的连接到期,应该从connections变量中删除。 But I don't find any reliable way to do that.但是我找不到任何可靠的方法来做到这一点。

Looking at the specification the objects of the connections variable doesn't seem to hold a property/function to check if the connection is still alive.查看规范connections变量的对象似乎没有属性/函数来检查连接是否仍然存在。

Is it possible?是否可以?
Again, the overall goal is to have the list of all windows/tabs.同样,总体目标是拥有所有窗口/选项卡的列表。

EDIT: An approach would be to make the shared worker message the windows and expect a reply.编辑:一种方法是让共享工作人员发送消息 windows 并期待回复。 If the shared worker doesn't receive a reply then it would assume that the window is closed.如果共享工作者没有收到回复,那么它会认为 window 已关闭。 In my experiments this approach has not shown to be reliable;在我的实验中,这种方法并不可靠; the problem being that there is no way to tell if a window is closed or is just taking a long time to reply.问题是无法判断 window 是否已关闭或只是需要很长时间才能回复。

This is only as reliable as beforeunload, but seems to work (tested in Firefox and Chrome).这仅与 beforeunload 一样可靠,但似乎有效(在 Firefox 和 Chrome 中测试)。 I definitely favour it over a polling solution.我绝对喜欢它而不是投票解决方案。

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

Then handle the cleanup of the port object in the SharedWorker.然后在 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;
    }
}

I have been neck deep in the documentation all week working around the same problem.我整个星期都在文档中深入研究相同的问题。

The problem is the MessagePort specification.问题是 MessagePort 规范。 The bad news being that it has no error handling, and no flag, method or event to determine whether it has been closed.坏消息是它没有错误处理,也没有标志、方法或事件来确定它是否已关闭。

The good news is I have created a viable solution, but it's a lot of code.好消息是我已经创建了一个可行的解决方案,但它有很多代码。

Keep in mind even among the supporting browsers the activity is handled differently.请记住,即使在支持的浏览器中,活动的处理方式也不同。 For example Opera will throw an error if you attempt to message or close a closed port.例如,如果您尝试发送消息或关闭关闭的端口,Opera 将抛出错误。 The bad news is you have to use a try-catch to handle the error, the good news is you can use that feedback to close a port on at least one-side.坏消息是您必须使用 try-catch 来处理错误,好消息是您可以使用该反馈至少在一侧关闭端口。

Chrome and Safari fail silently leaving you no feedback and no way to end invalid objects. Chrome 和 Safari 无声无息地失败,不会给您任何反馈,也无法结束无效对象。


My solution involves delivery confirmation or a custom "callback" approach.我的解决方案涉及交付确认或自定义“回调”方法。 You use a setTimeout and pass the ID for it to the SharedWorker with your command, and before processing the command it sends back a confirmation to cancel the timeout.您使用 setTimeout 并将其 ID 与您的命令一起传递给 SharedWorker,然后在处理该命令之前,它会发送回确认以取消超时。 That timeout is generally hooked to a closeConnection() method.该超时通常与 closeConnection() 方法挂钩。

This takes a reactive approach instead of a pre-emptive, originally I toyed with using the TCP/IP protocol model but that involved creating more functions to handle each process.这需要一种反应式方法而不是先发制人,最初我玩弄使用 TCP/IP 协议模型,但这涉及创建更多函数来处理每个进程。


Some Psuedo-Code as an example:一些伪代码作为例子:

Client/Tab 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 */ }
}

Thread/Worker Code:线程/工人代码:

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
}

I have placed a complete demonstration here: http://www.cdelorme.com/SharedWorker/我在这里放了一个完整的演示: http : //www.cdelorme.com/SharedWorker/

I am new to stack overflow, so I am not familiar with how they handle large code posts, but my full solution is two 150 line files.我是堆栈溢出的新手,所以我不熟悉他们如何处理大型代码帖子,但我的完整解决方案是两个 150 行的文件。


Just using delivery confirmation alone is not perfect, so I have worked at improving it by adding additional components.仅使用交付确认并不完美,因此我通过添加其他组件来改进它。

In particular I was investigating this for a ChatBox system, so I wanted to use EventSource (SSE), XHR, and WebSockets, only XHR is supported inside SharedWorker objects supposedly, which creates a limitation if I wanted to have the SharedWorker do all the server communication.特别是我正在为 ChatBox 系统调查这个,所以我想使用 EventSource (SSE)、XHR 和 WebSockets,据说 SharedWorker 对象内部只支持 XHR,如果我想让 SharedWorker 做所有服务器,这会产生限制沟通。

Plus since it needs to work for browsers without SharedWorker support I would be creating long-hand duplicate processing inside the SharedWorker which doesn't make a lot of sense.另外,因为它需要在没有 SharedWorker 支持的浏览器上工作,所以我将在 SharedWorker 内部创建长期重复处理,这没有多大意义。

So in the end if I implement SharedWorker it would be as a communication channel for the open tabs only, and one tab will be the Control Tab.所以最后,如果我实现 SharedWorker,它将仅作为打开选项卡的通信渠道,一个选项卡将是控制选项卡。

If the control tab is closed, the SharedWorker won't know, so I added a setInterval to the SharedWorker to send an empty response request every few seconds to all open ports.如果控制选项卡关闭,SharedWorker 将不知道,因此我向 SharedWorker 添加了一个 setInterval 以每隔几秒钟向所有打开的端口发送一个空响应请求。 This allows Chrome and Safari to eliminate closed connections when no messages are being processed, and allows the control tab to change.这允许 Chrome 和 Safari 在没有处理任何消息时消除关闭的连接,并允许更改控制选项卡。

However, this also means if the SharedWorker process dies the tabs must have an interval to check in with the SharedWorker using the same approach every so often, allowing them to use the fallback approach of every-tab-for-themeselves that is inherent to all other browsers using the same code.但是,这也意味着,如果 SharedWorker 进程终止,则选项卡必须有一个时间间隔以每隔一段时间使用相同的方法与 SharedWorker 签入,从而允许它们使用每个选项卡为主题所固有的回退方法其他浏览器使用相同的代码。


So, as you can see a combination of callbacks for delivery confirmation, setTimeout and setInterval must be used from both ends to maintain knowledge of connectivity.因此,正如您所看到的用于交付确认的回调组合,必须从两端使用 setTimeout 和 setInterval 来维护连接知识。 It can be done but it's a giant pain in the rear.这是可以做到的,但这是一个巨大的背部疼痛。

So there is actually a way to work out, what ports are still alive and whose are not.所以实际上有一种方法可以计算出哪些端口仍然存在,哪些不存在。 The key to the success is WeakRef and the part of "ports and garbage collection" portion of HTML standard, that cites how garbage collection work with MessagePort s.成功的关键是WeakRef和 HTML 标准的“端口和垃圾收集”部分,它引用了垃圾收集如何与MessagePort一起工作。

The full code is here: https://gist.github.com/Akxe/b4cfefa0086f9a995a3578818af63ad9完整代码在这里: https://gist.github.com/Akxe/b4cfefa0086f9a995a3578818af63ad9

The class is meant to be extendable to accommodate most use cases. class 旨在可扩展以适应大多数用例。

PortAwareSharedWorker

The PortAwareSharedWorker is a singleton that checks the alive (more below) status of each of its connected ports. PortAwareSharedWorker是一个 singleton,它检查每个连接端口的alive (更多信息见下文)状态。 It also exposes getOpenPorts method that contains, all current "alive" ports.它还公开了包含所有当前“活动”端口的getOpenPorts方法。

PortAwareSharedWorkerPort as replacement for MessagePort PortAwareSharedWorkerPort作为MessagePort的替代品

The MessagePort is never exposed to the user. MessagePort永远不会暴露给用户。 Instead, a wrapper with a weak reference is.相反,具有弱引用的包装器是。 It checks the validity of its reference with every action and handles edge some cases.它通过每个动作检查其引用的有效性,并处理某些情况。

Caveats注意事项

The solution is not bulletproof.该解决方案不是防弹的。 Weak references are resolvable even after the port should have been closed (no way around it).即使端口应该关闭(没有办法绕过它),弱引用也是可解析的。 To mitigate this the PortAwareSharedWorkerPort wrapper is provided, it will handle things like calling postMessage to an already closed port as it crashes in some browsers.为了缓解这种情况,提供了PortAwareSharedWorkerPort包装器,它将处理诸如调用postMessage到一个已经关闭的端口的事情,因为它在某些浏览器中崩溃了。

Also, the most noticeable, the spec states the following:此外,最值得注意的是,规范规定如下:

Furthermore, a MessagePort object must not be garbage collected while there exists an event referenced by a task in a task queue that is to be dispatched on that MessagePort object, or while the MessagePort object's port message queue is enabled and not empty.此外,MessagePort object 在任务队列中存在任务引用的事件时不得对 MessagePort object 进行垃圾回收,该事件将在 MessagePort object 上分派,或者 MessagePort 对象的端口消息队列已启用且不为空。

I am not 100% sure of the meaning of the quote, but my takeaway is that if within the onmessage callback the developer creates a long-running task that would result in further interaction with the port, the port cannot be closed.我不是 100% 确定引用的含义,但我的结论是,如果开发人员在onmessage回调中创建了一个长时间运行的任务,该任务会导致与端口的进一步交互,则无法关闭端口。 Ex.: Message comes in, a fetch request is made, and after it returns a response is sent back.例如:消息进来,发出一个获取请求,并在它返回后发回一个响应。 During the time that the fetch is being requested/processed, the port cannot be collected.在请求/处理提取期间,无法收集端口。

Please correct me if I am wrong, but I think that wrapping the message port in the WeakRef erence is enough to make it collectable again.如果我错了请纠正我,但我认为将消息端口包装在WeakRef中足以使其再次可收集。

PortCollection would come in handy but doesn't seem to be implemented in any browser. PortCollection会派上用场,但似乎没有在任何浏览器中实现。

It acts as an opaque array of MessagePort objects, thus allowing the objects to be garbage collected when they stop being relevant, while still allowing scripts to iterate over the MessagePort objects.它充当 MessagePort 对象的不透明数组,因此允许对象在停止相关时被垃圾收集,同时仍然允许脚本遍历 MessagePort 对象。

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

Edit;编辑; just rised an Issue for Chrome;刚刚为 Chrome 提出了一个问题; http://crbug.com/263356 http://crbug.com/263356

...How about using the approach you suggest in the edit, ie use a keep-alive ping, BUT: ...如何使用您在编辑中建议的方法,即使用保持活动的 ping,但是:

Just before closing any unresponsive connection, send a "please reconnect" message through it, so that if a window isn't really closed, just busy, it'll know it has to re-connect?就在关闭任何无响应的连接之前,通过它发送“请重新连接”消息,这样如果一个窗口没有真正关闭,只是忙,它就会知道它必须重新连接?

This technique should probably be combined with sending explicit "I'm closing now" messages from window onunload events, as per @Adria 's solution, so that normal window termination is handled efficiently and without any delay.根据@Adria 的解决方案,此技术可能应该与从窗口 onunload 事件发送显式“我正在关闭”消息相结合,以便有效地处理正常的窗口终止并且没有任何延迟。

This is still somewhat unreliable, in that very busy windows might drop off the SharedWorker's list temporarily, before later re-connecting... but actually I don't see how you could do much better: Consider that if a window hangs, practically speaking that's not going to be differentiable from a it being "busy" for some indefinitely long time, so you can't really catch one without catching the other (in any finite time, anyway).这仍然有些不可靠,因为非常繁忙的窗口可能会暂时从 SharedWorker 的列表中删除,然后再重新连接...这与无限期地“忙碌”一段时间没有区别,所以你不能真正抓住一个而不抓住另一个(无论如何,在任何有限的时间内)。

Depending on your application, having very busy windows temporarily getting de-listed may or may not be a big problem.根据您的应用程序,暂时将非常繁忙的窗口从列表中删除可能是也可能不是大问题。

Note that the keep-alive pings should be sent from the SharedWorker to windows, which should then respond: If you try simply using setTimout() in the windows, you run into the problem that setTimeout() on background windows can be long delayed (up to 1 second on current browsers I believe), while the SharedWorker's setTimeout()s should run on schedule (give or take a few ms), and idling background windows will wake up and respond immediately to posted SharedWorker messages.请注意,保持活动的 ping 应该从 SharedWorker 发送到窗口,然后窗口应该响应:如果您尝试在窗口中简单地使用 setTimout(),您会遇到后台窗口上的 setTimeout() 可能会延迟很长时间的问题(我相信在当前浏览器上最多 1 秒),而 SharedWorker 的 setTimeout()s 应该按计划运行(给予或花费几毫秒),并且空闲的后台窗口将被唤醒并立即响应发布的 SharedWorker 消息。


Here's a neat little demo of this technique, that:这是这种技术的一个简洁的小演示,即:

  1. Assigns each window a unique numerical ID为每个窗口分配一个唯一的数字 ID
  2. Keeps track of a single "active" window跟踪单个“活动”窗口
  3. Keeps track of the list of current window IDs, and the total count跟踪当前窗口 ID 的列表和总数
  4. Keeps all windows apprised of all the above at all times随时让所有窗口了解以上所有信息

sharedworker.html共享工作者.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 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共享工作者.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