...如何使用您在编辑中建议的方法,即使用保活 ping,但是:
就在关闭任何无响应的连接之前,通过它发送“请重新连接”消息,这样如果一个窗口没有真正关闭,只是忙,它会知道它必须重新连接?
根据@Adria 的解决方案,这种技术可能应该与从窗口 onunload 事件发送明确的“我现在正在关闭”消息相结合,以便有效地处理正常的窗口终止并且没有任何延迟。
这仍然有点不可靠,因为非常繁忙的窗口可能会暂时从 SharedWorker 的列表中删除,然后再重新连接......但实际上我看不出你能做得更好:考虑一下如果一个窗口挂起,实际上来说这与它无限期地“忙碌”很长一段时间没有区别,所以你不能真正抓住一个而不抓住另一个(无论如何,在任何有限的时间内)。
根据您的应用程序,让非常繁忙的窗口暂时被除名可能是也可能不是一个大问题。
请注意,保持活动的 ping 应从 SharedWorker 发送到 windows,然后应响应:如果您尝试在 windows 中简单地使用 setTimout(),您会遇到后台windows 上的 setTimeout() 可能会延迟很长时间的问题(我相信在当前浏览器上最多 1 秒),而 SharedWorker 的 setTimeout()s 应该按计划运行(给或需要几毫秒),空闲的背景窗口将唤醒并立即响应发布的 SharedWorker 消息。
这是该技术的一个简洁的小演示,即:
- 为每个窗口分配一个唯一的数字 ID
- 跟踪单个“活动”窗口
- 跟踪当前窗口 ID 的列表和总数
- 始终让所有窗口了解上述所有情况
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;