可以检测用户是否打开了您网站的多个选项卡吗?

发布于 2024-09-29 01:55:49 字数 739 浏览 3 评论 0原文

我只是在考虑整个网站注册过程。

用户访问您的网站,注册,然后您告诉他您已向他发送了一封电子邮件,他需要验证他的电子邮件地址。因此,他按下 Ctrl+T,弹出一个新选项卡,点击他最喜欢的 Gmail 按钮,没有读到您冗长的欢迎词电子邮件,但点击了他看到的第一个链接。 Gmail 会在另一个选项卡中打开您的网站...

他不需要也不希望您的网站打开两个选项卡,他只是想查看您不允许他访问的该死页面,直到他注册为止。

那么我们该怎么办呢?我看到一个网站(但我忘了它是什么)做得非常好,它实际上刷新了我打开的第一个选项卡,而无需我按任何内容。

我在想,如果我们能够检测用户是否已经打开了您网站的选项卡,那可能会很好,我们可以自动关闭新的验证选项卡,或者告诉他可以关闭它并返回到其他选项卡(我们现在已经刷新并让他登录了)。

或者,也许当他收到你烦人的“请检查你的电子邮件”消息时,他直接转到他的电子邮件,用他的电子邮件替换你的网站,他非常清楚该电子邮件将再次将他链接回该网站。在这种情况下,我们不想关闭该选项卡,但也许可以保存他之前的位置,然后再次将他重定向到那里?

无论如何,这只是用例......问题仍然存在。 我们可以检测用户是否已经打开了您网站的选项卡吗?


这个问题不是关于如何检测用户何时完成了注册过程。 Ajax 轮询或 comet 可以解决这个问题。我特别想知道用户是否已经打开了您网站的选项卡。

I'm just thinking about the whole site registration process.

A user goes to your site, signs up, and then you tell him you've sent him an email and he needs to verify his email address. So he hits Ctrl+T, pops open a new tab, hits his Gmail fav button, doesn't read a word of your lengthy welcome email, but clicks the first link he sees. Gmail opens your site in yet another tab...

He doesn't need nor want two tabs for your site open, he just wants to view that darn page you've disallowed him access to until he registers.

So what do we do? I saw one site (but I forget what it was) that did a really good job, and it actually refreshed the first tab I had open without me having to press anything.

I'm thinking, it might be nice if we can detect if the user already has a tab to your site open, we could either close the new verification-tab automatically, or tell him he can close it can go back to his other tab (which we've now refreshed and logged him in).

Or, maybe when he got your annoying "please check your email" message, he went directly to his email, replacing your site with his email knowing full well that the email will link him back to the site again. In that case, we don't want to close the tab, but maybe could have saved his location from before, and redirect him there again?

Anyway, that's just the use case... the question still stands. Can we detect if a user already has a tab to your site open?


This question is not about how to detect when a user has completed the sign-up process. Ajax polling or comet can solve that issue. I specifically want to know if the user already has a tab open to your site or not.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

黯然 2024-10-06 01:55:49

我参加这里的聚会相当晚(一年多了),但我忍不住注意到您错过了一个非常简单和优雅的解决方案(可能还有您看到的网站使用的解决方案)。

使用 JavaScript,您可以通过以下方式更改当前打开的窗口的名称:

window.name = "myWindow";

然后,当您发送确认电子邮件时,只需执行以下操作(假设您发送的是 HTML 电子邮件):

<a href="verificationlink.php" target="myWindow">Verify</a>

这应该会导致 verificationLink在您的网站已加载的窗口内打开,如果它已关闭,它将打开一个指定窗口名称的新选项卡。

I'm fairly late to the party here (over a year), but I couldn't help but notice that you'd missed an incredibly easy and elegant solution (and probably what that website you saw used).

Using JavaScript you can change the name of the window you currently have open through:

window.name = "myWindow";

Then when you send out your confirmation email simply do (assuming you're sending a HTML email):

<a href="verificationlink.php" target="myWindow">Verify</a>

Which should result in the verificationLink opening up inside the window your website was already loaded into, if it's already been closed it'll open up a new tab with the window name specified.

安静 2024-10-06 01:55:49

当用户打开另一个选项卡或另一个窗口甚至另一个浏览器时,您可以停止页面功能

$(window).blur(function(){
    // code to stop functioning or close the page  
});

You can stop the page functionality when user opened another tab or another window or even another browser

$(window).blur(function(){
    // code to stop functioning or close the page  
});
独行侠 2024-10-06 01:55:49

您可以从原始选项卡每隔 X 秒发送一个 AJAX 请求,询问服务器是否收到来自电子邮件的请求。

您无法自动关闭第二个选项卡,但您可以让它在 3X 秒后询问服务器是否收到第一个选项卡的消息。

You can send an AJAX request every X seconds from the original tab that asks the server if it received a request from the email.

You cannot close the second tab automatically, but you could have it ask the server after 3X seconds whether it heard from the first tab.

笑脸一如从前 2024-10-06 01:55:49

我这里的用例与您略有不同,但它会检测是否正在另一个选项卡中访问该网站。在这种情况下,我想限制人们仅使用一个选项卡来使用某些呼叫中心页面。它运行良好并且纯粹是客户端的。

// helper function to set cookies
function setCookie(cname, cvalue, seconds) {
    var d = new Date();
    d.setTime(d.getTime() + (seconds * 1000));
    var expires = "expires="+ d.toUTCString();
    document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

// helper function to get a cookie
function getCookie(cname) {
    var name = cname + "=";
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for(var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) == 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

// Do not allow multiple call center tabs
if (~window.location.hash.indexOf('#admin/callcenter')) {
    $(window).on('beforeunload onbeforeunload', function(){
        document.cookie = 'ic_window_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
    });

    function validateCallCenterTab() {
        var win_id_cookie_duration = 10; // in seconds

        if (!window.name) {
            window.name = Math.random().toString();
        }

        if (!getCookie('ic_window_id') || window.name === getCookie('ic_window_id')) {
            // This means they are using just one tab. Set/clobber the cookie to prolong the tab's validity.
            setCookie('ic_window_id', window.name, win_id_cookie_duration);
        } else if (getCookie('ic_window_id') !== window.name) {
            // this means another browser tab is open, alert them to close the tabs until there is only one remaining
            var message = 'You cannot have this website open in multiple tabs. ' +
                'Please close them until there is only one remaining. Thanks!';
            $('html').html(message);
            clearInterval(callCenterInterval);
            throw 'Multiple call center tabs error. Program terminating.';
        }
    }

    callCenterInterval = setInterval(validateCallCenterTab, 3000);
}

What I have here is a little bit different use case to you but it detects if the site is being accessed in another tab. In this case I wanted to limit people using some call center pages to only one tab. It works well and is purely client-side.

// helper function to set cookies
function setCookie(cname, cvalue, seconds) {
    var d = new Date();
    d.setTime(d.getTime() + (seconds * 1000));
    var expires = "expires="+ d.toUTCString();
    document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

// helper function to get a cookie
function getCookie(cname) {
    var name = cname + "=";
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for(var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) == 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

// Do not allow multiple call center tabs
if (~window.location.hash.indexOf('#admin/callcenter')) {
    $(window).on('beforeunload onbeforeunload', function(){
        document.cookie = 'ic_window_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
    });

    function validateCallCenterTab() {
        var win_id_cookie_duration = 10; // in seconds

        if (!window.name) {
            window.name = Math.random().toString();
        }

        if (!getCookie('ic_window_id') || window.name === getCookie('ic_window_id')) {
            // This means they are using just one tab. Set/clobber the cookie to prolong the tab's validity.
            setCookie('ic_window_id', window.name, win_id_cookie_duration);
        } else if (getCookie('ic_window_id') !== window.name) {
            // this means another browser tab is open, alert them to close the tabs until there is only one remaining
            var message = 'You cannot have this website open in multiple tabs. ' +
                'Please close them until there is only one remaining. Thanks!';
            $('html').html(message);
            clearInterval(callCenterInterval);
            throw 'Multiple call center tabs error. Program terminating.';
        }
    }

    callCenterInterval = setInterval(validateCallCenterTab, 3000);
}
清风夜微凉 2024-10-06 01:55:49

为了充实 John 的答案,这里有一个可行的解决方案,它使用纯 JS 和 localStorage 并使用当前打开的选项卡的数量更新 DOM。请注意,此解决方案检测一个浏览器中给定域打开的选项卡/窗口的数量,但不会维护不同浏览器之间的计数。

它使用存储事件来保持所有打开的选项卡/窗口之间的计数同步,而无需刷新页面。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title></title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="googlebot" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
(function() {
    var stor = window.localStorage;
    window.addEventListener("load", function(e) {
        var openTabs = stor.getItem("openTabs");
        if (openTabs) {
            openTabs++;
            stor.setItem("openTabs", openTabs)
        } else {
            stor.setItem("openTabs", 1)
        }
        render();
    })
    window.addEventListener("unload", function(e) {
        e.preventDefault();
        var openTabs = stor.getItem("openTabs");
        if (openTabs) {
            openTabs--;
            stor.setItem("openTabs", openTabs)
        }
        e.returnValue = '';
    });
    window.addEventListener('storage', function(e) {
        render();
    })

    function render() {
        var openTabs = stor.getItem("openTabs");
        var tabnum = document.getElementById("tabnum");
        var dname = document.getElementById("dname");
        tabnum.textContent = openTabs;
        dname.textContent = window.location.host
    }
}());
</script>
</head>
<body>
<div style="width:100%;height:100%;text-align:center;">
    <h1 >You Have<h1>
        <h1 id="tabnum">0</h1>
    <h1>Tab(s) of <span id="dname"></span> Open</h1>
</div>
</body>
</html>

To flesh out John's answer, here is a working solution that uses plain JS and localStorage and updates the DOM with the count of the currently open tabs. Note that this solution detects the number of open tabs/windows for a given domain within one browser, but does not maintain the count across different browsers.

It uses the storage event to keep the count synchronized across all open tabs/windows without any need for refreshing the page.

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title></title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="googlebot" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
(function() {
    var stor = window.localStorage;
    window.addEventListener("load", function(e) {
        var openTabs = stor.getItem("openTabs");
        if (openTabs) {
            openTabs++;
            stor.setItem("openTabs", openTabs)
        } else {
            stor.setItem("openTabs", 1)
        }
        render();
    })
    window.addEventListener("unload", function(e) {
        e.preventDefault();
        var openTabs = stor.getItem("openTabs");
        if (openTabs) {
            openTabs--;
            stor.setItem("openTabs", openTabs)
        }
        e.returnValue = '';
    });
    window.addEventListener('storage', function(e) {
        render();
    })

    function render() {
        var openTabs = stor.getItem("openTabs");
        var tabnum = document.getElementById("tabnum");
        var dname = document.getElementById("dname");
        tabnum.textContent = openTabs;
        dname.textContent = window.location.host
    }
}());
</script>
</head>
<body>
<div style="width:100%;height:100%;text-align:center;">
    <h1 >You Have<h1>
        <h1 id="tabnum">0</h1>
    <h1>Tab(s) of <span id="dname"></span> Open</h1>
</div>
</body>
</html>
一抹苦笑 2024-10-06 01:55:49

添加到其他答案:
您还可以使用本地存储。有一个像“openedTabs”这样的条目。当您的页面打开时,增加此数字。当用户离开页面时,减少它。

To add to other answers:
You can also use localStorage. Have an entry like 'openedTabs'. When your page is opened, increase this number. When user leaves the page, decrease it.

土豪 2024-10-06 01:55:49

用户仍将在服务器上拥有会话。为什么不在注册之前存储用户的位置,当用户确认注册时,从会话中读回该位置并重定向回该页面。不需要标签魔法。这当然不是我对注册过程的期望。

The user will still have a session at the server. Why not store the user's location prior to registration, and when they confirm their registration, read the location back out of the session and redirect back to that page. No tab magic required. It's certainly not what I'd expect from a signup process.

寄居人 2024-10-06 01:55:49

通过将数据保存在每个选项卡的 localstorage 中并进行计数,可以跟踪网站打开的选项卡数量,我创建了一个 github 存储库,它可以跟踪用户打开的网站选项卡数量。

要使用它,请在页面中包含 tab-counter.js,它将开始跟踪打开的选项卡数量。

console.log(tabCount.tabsCount());

It is possible to track number of tabs of your site opened by saving data in localstorage of each tab and counting the same, I created a github repository which can track number of tabs of your website a user has opened.

To use it Include tab-counter.js in your page and it will start tracking number of opened tabs.

console.log(tabCount.tabsCount());
你是年少的欢喜 2024-10-06 01:55:49

这是一个使用广播频道进行跨选项卡通信的系统。它还为每个选项卡分配一个唯一的 ID,并管理已打开的选项卡和新选项卡的发现。最后,使用 ID 作为稳定索引,它允许用户重命名其选项卡。选项卡关闭事件也通过轮询处理(卸载事件不可靠)。

这通过构造函数中的回调插入到 redux 中。在此示例中,这些是 onNewTabonDestroyTabonRenameTab

import { setTabs } from './redux/commonSlice';
import { store } from './redux/store';

const promiseTimeout = (ms, promise) => {
    let id;
    let timeout = new Promise((resolve, reject) => {
        id = setTimeout(() => {
            reject('Timed out in ' + ms + 'ms.');
        }, ms)
    })

    return Promise.race([
        promise,
        timeout
    ]).then((result) => {
        clearTimeout(id);
        return result;
    })
};

// Promise that can be resolved/rejected outside of its constructor. Like a signal an async event has occured.
class DeferredPromise {
    constructor() {
        this._promise = new Promise((resolve, reject) => {
            // assign the resolve and reject functions to `this`
            // making them usable on the class instance
            this.resolve = resolve;
            this.reject = reject;
        });
        // bind `then` and `catch` to implement the same interface as Promise
        this.then = this._promise.then.bind(this._promise);
        this.catch = this._promise.catch.bind(this._promise);
        this.finally = this._promise.finally.bind(this._promise);
        this[Symbol.toStringTag] = 'Promise';
    }
}

class TabManager {
    tabCreateCallback = undefined;
    tabDestroyCallback = undefined;
    tabRenameCallback = undefined;

    constructor(onNewTab, onDestroyTab, onRenameTab) {
        this.tabCreateCallback = onNewTab.bind(this);
        this.tabDestroyCallback = onDestroyTab.bind(this);
        this.tabRenameCallback = onRenameTab.bind(this);

        // creation time gives us a total ordering of open tabs, also acts as a tab ID
        this.creationEpoch = Date.now();
        this.channel = new BroadcastChannel("TabManager");
        this.channel.onmessage = this.onMessage.bind(this);

        // our current tab (self) counts too
        this.tabs = [];
        this.tabNames = {};

        // start heartbeats. We check liveness like this as there is _no_ stable browser API for tab close.
        // onbeforeunload is not reliable in all situations.
        this.heartbeatPromises = {};
        this.heartbeatIntervalMs = 1000;
        setTimeout(this.doHeartbeat.bind(this), this.heartbeatIntervalMs);
    }

    doComputeNames() {
        for (let i = 0; i < this.tabs.length; i++) {
            const tab = this.tabs[i];
            const name = this.tabNames[tab];
            const defaultName = `Tab ${i + 1}`;
            if (!name) {
                this.tabNames[tab] = defaultName;

                if (this.tabRenameCallback) {
                    this.tabRenameCallback(tab, name);
                }
                // if it's a default pattern but wrong inde value, rename it
            } else if (name && this.isDefaultName(name) && name !== defaultName) {
                this.tabNames[tab] = defaultName;

                if (this.tabRenameCallback) {
                    this.tabRenameCallback(tab, name);
                }
            }
        }
    }

    doHeartbeat() {
        for (let tab of this.tabs) {
            if (tab === this.creationEpoch) {
                continue;
            }

            this.channel.postMessage({ type: "heartbeat_request", value: tab });

            const heartbeatReply = new DeferredPromise();
            heartbeatReply.catch(e => { });

            // use only a fraction of poll interval to ensure timeouts occur before poll. Prevents spiral of death.
            let heartbeatReplyWithTimeout = promiseTimeout(this.heartbeatIntervalMs / 3, heartbeatReply);

            // destroy tab if heartbeat times out
            heartbeatReplyWithTimeout.then(success => {
                delete this.heartbeatPromises[tab];
            }).catch(error => {
                delete this.heartbeatPromises[tab];

                this.tabs = this.tabs.filter(id => id !== tab);
                this.tabs.sort();

                this.doComputeNames();
                if (this.tabDestroyCallback) {
                    this.tabDestroyCallback(tab);
                }
            });

            this.heartbeatPromises[tab] = heartbeatReply;
        }

        // re-schedule to loop again
        setTimeout(this.doHeartbeat.bind(this), this.heartbeatIntervalMs);
    }

    doInitialize() {
        this.tabs = [this.creationEpoch];
        this.doComputeNames();
        if (this.tabCreateCallback) {
            this.tabCreateCallback(this.creationEpoch);
        }
        this.channel.postMessage({ type: "creation", value: this.creationEpoch });
    }

    onMessage(event) {
        if (event.data.type == "creation") {
            const newTabId = event.data.value;

            // add the new tab
            if (!this.tabs.includes(newTabId)) {
                this.tabs.push(newTabId);
                this.tabs.sort();
                this.doComputeNames();
                if (this.tabCreateCallback) {
                    this.tabCreateCallback(newTabId);
                }
            }

            // send all of the tabs we know about to it
            this.channel.postMessage({ type: "syncnew", value: this.tabs });

            // those tabs we just sent might already have custom names, lets send the older rename requests
            // which would have had to have occured. I.E. lets replay forward time and sync the states of ours to theirs.
            for (let tab of this.tabs) {
                const name = this.tabNames[tab];
                if (name && !this.isDefaultName(name)) {
                    this.notifyTabRename(tab, name);
                }
            }
        } else if (event.data.type == "syncnew") {
            let newTabs = [];

            // just got a list of new tabs add them if we down't know about them
            for (let id of event.data.value) {
                if (!this.tabs.includes(id)) {
                    newTabs.push(id);
                }
            }

            // merge the lists and notify of only newly discovered
            if (newTabs.length) {
                this.tabs = this.tabs.concat(newTabs);
                this.tabs.sort();
                this.doComputeNames();

                for (let id of newTabs) {
                    if (this.tabCreateCallback) {
                        this.tabCreateCallback(id);
                    }
                }
            }
        } else if (event.data.type == "heartbeat_request") {
            // it's for us, say hi back
            if (event.data.value === this.creationEpoch) {
                this.channel.postMessage({ type: "heartbeat_reply", value: this.creationEpoch });
            }
        } else if (event.data.type == "heartbeat_reply") {
            // got a reply, cool resolve the heartbeat
            if (this.heartbeatPromises[event.data.value]) {
                // try catch since this is racy, entry may have timed out after this check passed
                try {
                    this.heartbeatPromises[event.data.value].resolve();
                } catch {

                }
            }
        } else if (event.data.type == "rename") {
            // someone renamed themselves, lets update our record
            const { id, name } = event.data.value;
            if (this.tabs.includes(id)) {
                this.tabNames[id] = name;

                // first original (potentially illegal) rename callback first
                if (this.tabRenameCallback) {
                    this.tabRenameCallback(id, name);
                }

                // force tab numbers back to consistent
                this.doComputeNames();
            }
        }
    }

    setTabName(id, name) {
        if (this.tabs.includes(id)) {
            this.tabNames[id] = name;
            this.notifyTabRename(id, name);

            if (this.tabRenameCallback) {
                this.tabRenameCallback(id, name);
            }

            // force tab numbers back to consistent
            this.doComputeNames();
        }
    }

    notifyTabRename(id, name) {
        this.channel.postMessage({ type: "rename", value: { id, name } });
    }

    isDefaultName(name) {
        return name.match(/Tab [0-9]+/)
    }

    getMyTabId() {
        return this.creationEpoch;
    }

    getMyTabIndex() {
        return this.tabs.findIndex(tab => tab === this.creationEpoch);
    }

    isMyTab(id) {
        return id === this.creationEpoch;
    }

    getAllTabs() {
        return this.tabs.map((tab, idx) => {
            return { id: tab, index: idx, name: this.tabNames[tab] ?? "" };
        }, this);
    }
}

function onDestroyTab(id) {
    store.dispatch(setTabs(this.getAllTabs()));
    console.log(`Tab ${id} destroyed`);
}

function onNewTab(id) {
    store.dispatch(setTabs(this.getAllTabs()));
    console.log(`Tab ${id} created`);
}

function onRenameTab(id, name) {
    store.dispatch(setTabs(this.getAllTabs()));
    console.log(`Tab ${id} renamed to ${name}`);
}

const TabManager = new TabManager(onNewTab, onDestroyTab, onRenameTab);
export default TabManager;

在页面加载时对其进行初始化

window.addEventListener("DOMContentLoaded", function (event) {
    TabManager.doInitialize();
});

随时访问静态对象上的任何方法。请注意,您可以从创建/销毁中乱序地获取重命名事件。这个问题可以解决,但这对我来说并不重要。

Here's a system that uses broadcast channels for cross tab comms. It also assigns a unique ID per tab and manages the discovery of already opened tabs, for new tabs. Finally, using the ID as a stable index, it allows the user to rename their tabs. Tab closing events are handled via polling as well (unload events are unreliable).

This plugs into redux via the callbacks in the constructor. These are onNewTab, onDestroyTab, onRenameTab in this example.

import { setTabs } from './redux/commonSlice';
import { store } from './redux/store';

const promiseTimeout = (ms, promise) => {
    let id;
    let timeout = new Promise((resolve, reject) => {
        id = setTimeout(() => {
            reject('Timed out in ' + ms + 'ms.');
        }, ms)
    })

    return Promise.race([
        promise,
        timeout
    ]).then((result) => {
        clearTimeout(id);
        return result;
    })
};

// Promise that can be resolved/rejected outside of its constructor. Like a signal an async event has occured.
class DeferredPromise {
    constructor() {
        this._promise = new Promise((resolve, reject) => {
            // assign the resolve and reject functions to `this`
            // making them usable on the class instance
            this.resolve = resolve;
            this.reject = reject;
        });
        // bind `then` and `catch` to implement the same interface as Promise
        this.then = this._promise.then.bind(this._promise);
        this.catch = this._promise.catch.bind(this._promise);
        this.finally = this._promise.finally.bind(this._promise);
        this[Symbol.toStringTag] = 'Promise';
    }
}

class TabManager {
    tabCreateCallback = undefined;
    tabDestroyCallback = undefined;
    tabRenameCallback = undefined;

    constructor(onNewTab, onDestroyTab, onRenameTab) {
        this.tabCreateCallback = onNewTab.bind(this);
        this.tabDestroyCallback = onDestroyTab.bind(this);
        this.tabRenameCallback = onRenameTab.bind(this);

        // creation time gives us a total ordering of open tabs, also acts as a tab ID
        this.creationEpoch = Date.now();
        this.channel = new BroadcastChannel("TabManager");
        this.channel.onmessage = this.onMessage.bind(this);

        // our current tab (self) counts too
        this.tabs = [];
        this.tabNames = {};

        // start heartbeats. We check liveness like this as there is _no_ stable browser API for tab close.
        // onbeforeunload is not reliable in all situations.
        this.heartbeatPromises = {};
        this.heartbeatIntervalMs = 1000;
        setTimeout(this.doHeartbeat.bind(this), this.heartbeatIntervalMs);
    }

    doComputeNames() {
        for (let i = 0; i < this.tabs.length; i++) {
            const tab = this.tabs[i];
            const name = this.tabNames[tab];
            const defaultName = `Tab ${i + 1}`;
            if (!name) {
                this.tabNames[tab] = defaultName;

                if (this.tabRenameCallback) {
                    this.tabRenameCallback(tab, name);
                }
                // if it's a default pattern but wrong inde value, rename it
            } else if (name && this.isDefaultName(name) && name !== defaultName) {
                this.tabNames[tab] = defaultName;

                if (this.tabRenameCallback) {
                    this.tabRenameCallback(tab, name);
                }
            }
        }
    }

    doHeartbeat() {
        for (let tab of this.tabs) {
            if (tab === this.creationEpoch) {
                continue;
            }

            this.channel.postMessage({ type: "heartbeat_request", value: tab });

            const heartbeatReply = new DeferredPromise();
            heartbeatReply.catch(e => { });

            // use only a fraction of poll interval to ensure timeouts occur before poll. Prevents spiral of death.
            let heartbeatReplyWithTimeout = promiseTimeout(this.heartbeatIntervalMs / 3, heartbeatReply);

            // destroy tab if heartbeat times out
            heartbeatReplyWithTimeout.then(success => {
                delete this.heartbeatPromises[tab];
            }).catch(error => {
                delete this.heartbeatPromises[tab];

                this.tabs = this.tabs.filter(id => id !== tab);
                this.tabs.sort();

                this.doComputeNames();
                if (this.tabDestroyCallback) {
                    this.tabDestroyCallback(tab);
                }
            });

            this.heartbeatPromises[tab] = heartbeatReply;
        }

        // re-schedule to loop again
        setTimeout(this.doHeartbeat.bind(this), this.heartbeatIntervalMs);
    }

    doInitialize() {
        this.tabs = [this.creationEpoch];
        this.doComputeNames();
        if (this.tabCreateCallback) {
            this.tabCreateCallback(this.creationEpoch);
        }
        this.channel.postMessage({ type: "creation", value: this.creationEpoch });
    }

    onMessage(event) {
        if (event.data.type == "creation") {
            const newTabId = event.data.value;

            // add the new tab
            if (!this.tabs.includes(newTabId)) {
                this.tabs.push(newTabId);
                this.tabs.sort();
                this.doComputeNames();
                if (this.tabCreateCallback) {
                    this.tabCreateCallback(newTabId);
                }
            }

            // send all of the tabs we know about to it
            this.channel.postMessage({ type: "syncnew", value: this.tabs });

            // those tabs we just sent might already have custom names, lets send the older rename requests
            // which would have had to have occured. I.E. lets replay forward time and sync the states of ours to theirs.
            for (let tab of this.tabs) {
                const name = this.tabNames[tab];
                if (name && !this.isDefaultName(name)) {
                    this.notifyTabRename(tab, name);
                }
            }
        } else if (event.data.type == "syncnew") {
            let newTabs = [];

            // just got a list of new tabs add them if we down't know about them
            for (let id of event.data.value) {
                if (!this.tabs.includes(id)) {
                    newTabs.push(id);
                }
            }

            // merge the lists and notify of only newly discovered
            if (newTabs.length) {
                this.tabs = this.tabs.concat(newTabs);
                this.tabs.sort();
                this.doComputeNames();

                for (let id of newTabs) {
                    if (this.tabCreateCallback) {
                        this.tabCreateCallback(id);
                    }
                }
            }
        } else if (event.data.type == "heartbeat_request") {
            // it's for us, say hi back
            if (event.data.value === this.creationEpoch) {
                this.channel.postMessage({ type: "heartbeat_reply", value: this.creationEpoch });
            }
        } else if (event.data.type == "heartbeat_reply") {
            // got a reply, cool resolve the heartbeat
            if (this.heartbeatPromises[event.data.value]) {
                // try catch since this is racy, entry may have timed out after this check passed
                try {
                    this.heartbeatPromises[event.data.value].resolve();
                } catch {

                }
            }
        } else if (event.data.type == "rename") {
            // someone renamed themselves, lets update our record
            const { id, name } = event.data.value;
            if (this.tabs.includes(id)) {
                this.tabNames[id] = name;

                // first original (potentially illegal) rename callback first
                if (this.tabRenameCallback) {
                    this.tabRenameCallback(id, name);
                }

                // force tab numbers back to consistent
                this.doComputeNames();
            }
        }
    }

    setTabName(id, name) {
        if (this.tabs.includes(id)) {
            this.tabNames[id] = name;
            this.notifyTabRename(id, name);

            if (this.tabRenameCallback) {
                this.tabRenameCallback(id, name);
            }

            // force tab numbers back to consistent
            this.doComputeNames();
        }
    }

    notifyTabRename(id, name) {
        this.channel.postMessage({ type: "rename", value: { id, name } });
    }

    isDefaultName(name) {
        return name.match(/Tab [0-9]+/)
    }

    getMyTabId() {
        return this.creationEpoch;
    }

    getMyTabIndex() {
        return this.tabs.findIndex(tab => tab === this.creationEpoch);
    }

    isMyTab(id) {
        return id === this.creationEpoch;
    }

    getAllTabs() {
        return this.tabs.map((tab, idx) => {
            return { id: tab, index: idx, name: this.tabNames[tab] ?? "" };
        }, this);
    }
}

function onDestroyTab(id) {
    store.dispatch(setTabs(this.getAllTabs()));
    console.log(`Tab ${id} destroyed`);
}

function onNewTab(id) {
    store.dispatch(setTabs(this.getAllTabs()));
    console.log(`Tab ${id} created`);
}

function onRenameTab(id, name) {
    store.dispatch(setTabs(this.getAllTabs()));
    console.log(`Tab ${id} renamed to ${name}`);
}

const TabManager = new TabManager(onNewTab, onDestroyTab, onRenameTab);
export default TabManager;

Initialize it on page load

window.addEventListener("DOMContentLoaded", function (event) {
    TabManager.doInitialize();
});

Access any of the methods on the static object at any time. Note that you can get rename events out of order from create / destroy. This could be resolved, but it wasn't important for me.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文