Можно ли определить, если у пользователя открыто несколько вкладок вашего сайта?

Я просто думаю о процессе регистрации всего сайта.

Пользователь заходит на ваш сайт, регистрируется, а затем вы сообщаете ему, что отправили ему электронное письмо, и он должен подтвердить свой адрес электронной почты. Поэтому он нажимает Ctrl+T, открывает новую вкладку, нажимает его Gmail Кнопка fav, не читает ни слова из вашего длинного приветственного письма, но нажимает первую ссылку, которую видит. Gmail открывает ваш сайт в еще одной вкладке...

Ему не нужно и не нужно открывать две вкладки для вашего сайта, он просто хочет просмотреть эту чертову страницу, к которой вы запретили ему доступ, пока он не зарегистрируется.

Так что же нам делать? Я видел один сайт (но я забыл, что это было), который сделал действительно хорошую работу, и он фактически обновил первую открытую вкладку, без необходимости что-либо нажимать.

Я думаю, было бы неплохо, если бы мы могли определить, есть ли у пользователя открытая вкладка для вашего сайта, мы можем либо автоматически закрыть новую вкладку проверки, либо сказать ему, что он может закрыть ее, чтобы вернуться к другой вкладке. (который мы сейчас обновили и вошли в него).

Или, может быть, когда он получил ваше раздражающее сообщение "проверьте свою электронную почту", он сразу перешел на свою электронную почту, заменив ваш сайт своей электронной почтой, прекрасно зная, что электронная почта снова свяжет его с сайтом. В этом случае мы не хотим закрывать вкладку, но, возможно, могли бы сохранить его местоположение и перенаправить его туда снова?

Во всяком случае, это всего лишь вариант использования... вопрос все еще стоит. Можем ли мы определить, есть ли у пользователя открытая вкладка на вашем сайте?


Этот вопрос не о том, как определить, когда пользователь завершил процесс регистрации. Ajax Polling или комета могут решить эту проблему. Я специально хочу знать, есть ли у пользователя открытая вкладка для вашего сайта или нет.

9 ответов

Решение

Я довольно опоздал на вечеринку здесь (более года), но я не мог не заметить, что вы пропустили невероятно простое и элегантное решение (и, вероятно, какой веб-сайт вы видели).

Используя JavaScript, вы можете изменить имя окна, которое вы открыли в данный момент:

window.name = "myWindow";

Затем, когда вы отправляете письмо с подтверждением, просто делаете (при условии, что вы отправляете письмо в формате HTML):

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

Что должно привести к verificationLink Если открыть окно, в которое ваш сайт уже загружен, то если оно уже закрыто, откроется новая вкладка с указанным именем окна.

Вы можете остановить функциональность страницы, когда пользователь открыл другую вкладку или другое окно или даже другой браузер

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

Вы можете отправлять запрос AJAX каждые X секунд с исходной вкладки, которая спрашивает сервер, получил ли он запрос от электронного письма.

Вы не можете автоматически закрыть вторую вкладку, но вы можете попросить сервер через 3X секунды услышать, услышала ли она первую вкладку.

То, что у меня здесь есть, немного отличается от вас, но он определяет, осуществляется ли доступ к сайту в другой вкладке. В этом случае я хотел ограничить людей, использующих некоторые страницы центра обработки вызовов, только одной вкладкой. Это работает хорошо и чисто на стороне клиента.

// 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);
}

Чтобы конкретизировать ответ Джона, вот рабочее решение, которое использует простой 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>

Чтобы добавить к другим ответам: Вы также можете использовать localStorage. Иметь такую ​​запись, как "OpenTabs". Когда ваша страница открыта, увеличьте это число. Когда пользователь покидает страницу, уменьшите ее.

У пользователя все еще будет сеанс на сервере. Почему бы не сохранить местоположение пользователя до регистрации, и когда они подтвердят свою регистрацию, прочитайте местоположение вне сеанса и перенаправьте обратно на эту страницу. Никакой магии не требуется. Это, конечно, не то, что я ожидаю от процесса регистрации.

Можно отслеживать количество открытых вкладок вашего сайта, сохранив данные в localstorage Для каждой вкладки, считая то же самое, я создал репозиторий github, который может отслеживать количество вкладок вашего сайта, открытых пользователем.

Чтобы использовать его, включите tab-counter.js на своей странице, и он начнет отслеживать количество открытых вкладок.

console.log(tabCount.tabsCount());

Вот система, которая использует широковещательные каналы для перекрестных сообщений. Он также назначает уникальный идентификатор для каждой вкладки и управляет обнаружением уже открытых вкладок для новых вкладок. Наконец, используя идентификатор в качестве стабильного индекса, пользователь может переименовывать свои вкладки. События закрытия вкладок также обрабатываются с помощью опроса (события выгрузки ненадежны).

Это подключается к редуксу через обратные вызовы в конструкторе. Это onNewTab, onDestroyTab, onRenameTabв этом примере.

      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();
});

Получите доступ к любому из методов статического объекта в любое время. Обратите внимание, что вы можете получить события переименования не по порядку при создании/уничтожении. Это можно было решить, но для меня это было не важно.

Другие вопросы по тегам