Задача коммуникации между вкладками и выявления активной вкладки

  • Tutorial
Наглядный пример задачи — сайт vk.com. Каждый раз, когда вы воспроизводите музыку или видео в одной вкладке, в других вкладках воспроизведение останавливается. И если вы обратитесь в интернет за помощью в решении данной задачи, то наверняка найдете описание Storage Events или Page Visibility API или даже готовые решения, к примеру Visibility.js.

На хабре уже был обзор этих вещей, к примеру вот и ещё.



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

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

Активная вкладка


Удобная кроссбраузерная обертка для простого отражения состояния «эта вкладка сейчас активна», в visibility.js будет выглядеть следующим образом:

if ('visible' == Visibility.state()) {
    // эта вкладка активна
}

У вкладки Visibility есть 3 состояния: visible, hidden и prerender.

Преимуществом Visibility также являются callbacks, которые вы можете вешать на множество событий, таких как активизация вкладки, setInterval в активном окне и тому подобное.

Философия DuelJS слегка упрощена:
1. Все вкладки имеют лишь 2 состояния — Master и Slave
2. Мастер вкладка это вкладка в которой ведется работа — ничего лишнего, все остальные есть Slave.

При таком подходе достаточно лишь одной функции: window.isMaster() — проверить является ли вкладка мастером.

if (window.isMaster()) {
    // эта вкладка активна
}

Коммуникации между вкладками


Теперь перейдем к коммуникациям между вкладками. Наиболее подходящим решением мне показалось использование Storage Events, хотя они и не без проблем. К слову находил я в гугле еще такие варианты как использование postMessage API или WebSockets.

Основная проблема Storage Events заключается в их плохой поддержке некоторыми MSIE, хотя до недавнего времени и у других браузеров тоже могли возникать с этим проблемы.

Так как Visibility.js является по сути оберткой над Page Visibility API — работа со Storage Events в ней отсутствует.

В DuejJS существует кроссбраузерная обертка над Storage Events, которая выражается в следующей философии:
1. Коммуникации между вкладками осуществляются при помощи каналов
2. Внутри канала вкладки могут запускать события, на которые могут реагировать другие вкладки в этом канале

Создать канал достаточно просто:

var ch = duel.channel('channel_name'); // channel_name - имя канала

Теперь определим поведение при вызове события qwerty:

ch.on('qwerty', function (a, b, c, ...) {})

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

Запустить событие так же легко. Слово on теперь заменим на broadcast, а передаваемые аргументы вставим после названия события:

ch.broadcast('qwerty', a, b, c, ...)


Создание плеера с поведением как на vk.com


Для нетерпеливых читателей сразу даю ссылку на рабочий пример.

Основная суть приложения заключается в трех строках:
1. var player = duel.channel('player'); (определение канала)
2. player.on('stop', function () {… (определение поведения при событии stop)
3. player.broadcast('stop'); (запуск события stop)

Полный код страницы выглядит следующим образом:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Остановлено</title>
    <!-- Заголовок (title) страницы отражает состояние воспроизведения. -->

    <script type="text/javascript" src="duel.min.js"></script>
    <link rel="stylesheet" type="text/css" href="pretty.css">
</head>
<body>
    <!-- div #preview представляет из себя кликабельную картинку, при клике по которой инициализируется iframe с роликом youtube -->
    <div id="preview"><div>PLAY</div></div>
    <a href="index.html" target="_blank">Открыть новую вкладку</a>
    <script type="text/javascript">
    /** открываем канал с именем player */
    var player = duel.channel('player');

    /** превью для видео-ролика */
    var previewDiv = document.getElementById('preview');

    /**
     * Регистрируем новое событие канала player
     */
    player.on('stop', function () {
        /**
         * Удаляем iframe
         */
        var frame = document.getElementsByTagName('iframe')[0];
        frame.parentNode.removeChild(frame);

        /**
         * Показываем превью
         */
        previewDiv.style.display = 'block';

        /**
         * Обновляем заголовок вкладки
         */
        document.title = 'Остановлено';
    });

    previewDiv.onclick = function () {
        /**
         * Посылаем сигнал stop в канал player
         */
        player.broadcast('stop');

        /**
         * Создаем новый элемент iframe с видео из youtube
         */
        var frame = document.createElement("iframe");
        frame.width = '859';
        frame.height = '480';
        frame.src = '//www.youtube.com/embed/xsV8TrF4gN0?rel=0&autoplay=1';
        frame.frameborder = '0';

        /**
         * Встраиваем его под превью и скрываем превью
         */
        previewDiv.parentNode.insertBefore(frame, previewDiv.nextSibling);
        previewDiv.style.display = 'none';

        /**
         * Обновляем заголовок вкладки
         */
        document.title = 'Играем...';
    }
    </script>
</body>
</html>



Заключение


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



Полезные ссылки


Краткая документация DuelJS на сайте
Репозиторий DuelJS на GitHub
Ещё одна демка DuelJS
Документация на readthedocs
Поделиться публикацией

Похожие публикации

Комментарии 24

    –5
    Все вкладки имеют лишь 2 состояния — Master и Slave

    Сразу вспомнился срачь Replace «master/slave» terminology with «primary/replica»
      +4
      Некоторое время назад (может и сейчас), VK использовал LocalStorage для коммуникации между вкладками. Он постоянно перезаписывал хранилище, чтобы другие вкладки могли прочитать. Из-за этого как только я заходит на VK (даже если всего одна вкладка), мой жёсткий диск начинал тихонько трещать, не прекращая, что очень сильно раздражало. Писал в техподдержку, сказали ничего сделать не могут. Покопался в JS коде и нашёл как отключить постоянную работу с LS, приходилось каждый раз писать команду в консоли. Хорошо хоть VK мне редко нужен был. Так и жил, пока не купил SSD.
        0
        Так написали бы юзерскрипт!
          0
          Я попытался, но почему-то не работало. Не стал разбираться.
          0
          Поделитесь командой для консоли?
            0
            Извините, уже не помню. Она была в истории команд браузера.
              0
              Эта тема меня заинтересовала.

              Написал пару небольших юзерскриптов. Один подменяет localStorage, но не теряет данные, другой — удаляет его с помощью
              Object.defineProperty(window, 'localStorage', { 'value': undefined });
              
              0
              До сих пор использует и мне жалко свой SSD. Каждую секунду перезапись queue_connection_events_queue.
              Пока слишком грязно и быстро — пустой объект

              curNotifier = {}


              Смотреть нужно notitfier.js.
                0
                Правильнее конечно использовать метод destory

                Notifier.destroy()
                


                А для расширения (хрома например) если быстро — создаем тег script и там уже выполняем.
                0
                Добрый день.
                А про алгоритм выбора мастера не понятно.
                Я так понял мастер это всегда видимая вкладка, так?
                А что если уйти от этого, чтоб мастером была одна из открытых и при ее закрытии снова искался мастер?
                Что если мастер должен делать какие то сложные и долгие операции, и лишнее переопределение мастера за собой потянет задержку в работе и лишнюю нагрузку?
                  0
                      » если мастер должен делать какие то сложные и долгие операции
                  По-моему, это противоречит философии мастера.
                    0
                    :)
                    Ну вот, например, подключение по веб сокету.
                    Стал мастером подключился, стал слейвом отключился.
                    Или, например, использование webRtc в мастере.
                      0
                      Когда я писал js api для комет сервера тоже искал мастер вкладку, в моей реализации как раз мастер держал websockets соединение. Но вовсе не обязательно что бы соединение держала именно видимая вкладка. По этому мастером в моём случаи была первая открытая вкладка, а если её закрывали то та из вкладок которая первой обнаруживала закрытие мастер вкладки.
                      Если интересно я могу раскрыть эту тему подробно с примерами но в рамках отдельной статьи.
                        0
                        У меня уже есть похожая реализация, примерно так как вы описали.
                        Да, конечно, было бы интересно посмотреть раскрытие этой темы, заодно сравнить со своей реализацией.
                    0
                    Есть риск того, что браузер как-то обделит неактивную вкладку, да и это немного не та задача, которую решает моя библиотека.
                    0
                    Тоже писал такое. github.com/StreetStrider/XTab

                    API: on, off, once.
                      0
                      Неплохо, но на каких браузерах вы тестировали корректную работу вашей библиотеки? Например IE сейчас ведет себя некорректно в некоторых случаях. Метод window.addEventListener тоже не кроссбраузерный. Как себя поведет ваша библиотека, если localStorage по каким-то причинам не работает?
                        0
                        Я не проверял её во всех возможных окружениях. Это что-то вроде минималистичной реализации, которая полагается на все существующие API.
                      0
                      Есть вот это github.com/RubaXa/wormhole
                        –2
                        Как я понял из исходников, оно работает не только со storage. Честно говоря решение показалось мне слегка перегруженным, хотя конечно это дело вкуса. Видно что работы проделано немало и ещё сделана поддержка cross-origin resource sharing. Можете вы или StreetStrider привести примеры использования биндинга «once» в рабочих условиях? Что-то я до этого даже и не додумался.
                          0
                          Например, у нас есть нотификация, которая прилетает во все вкладки. Мы можем её принудительно закрыть в одной из вкладок, и она должна закрываться во всех. Тогда при конструировании нотификации можно повесить одноразовый обработчик на событие типа «нотификацию закрыли».

                          once хорош тем, что высвобождая функцию он высвобождает все её объекты scope, то бишь, высвобождает замыкания (если в самом замыкании нет ссылки на эту функцию, хе-хе) и всё, что в них было.
                        +1
                        На тему коммуникаций между вкладками просто оставлю ссылку на свой пост почти годичной давности: Библиотека для обмена событиями, данными и задачами между вкладками браузера.
                          0
                          Да, у вас получилось очень широкое обилие возможностей. Честно говоря немного странный у вас стиль кода, но труда вы вложили довольно много, это видно сразу. Хотя некоторые элементы показались мне лишними, к примеру зачем мне для простой задачи коммуникации знать сколько у меня открыто вкладок, или событие при закрытии вкладки — оно реализуется в моем случае примерно так: duel.addEvent('beforeunload', function ()… Тут по сути vanilla js, а duel.addEvent — лишь кросс-браузерная обертка над window.addEventListener.
                          Если к примеру потребуется, чтобы остальные вкладки узнали о том, что одна из вкладок закрылась — их можно оповестить и даже с WindowID, который, опять таки, в простой задаче не требуется:

                          duel.addEvent('beforeunload', function () {
                              someChannel.broadcast('one_of_tabs_are_closed', duel.getWindowID());
                          });
                          
                          someChannel.on('one_of_tabs_are_closed', function (tabID) {
                              console.log('on of tabs are closed:', tabID);
                          });
                            0
                            Немного ошибся: duel.addEvent(window, 'beforeunload'

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

                        Самое читаемое