Реализация обмена сообщениями между вкладками браузера

в 13:25, , рубрики: ajax, comet, javascript, web-разработка, websockets, Блог компании Star.Comet, Веб-разработка, Клиентская оптимизация

Это первая статья в нашем корпоративном блоге. На этот раз я расскажу о нашем решении задачи обмена сообщениями между вкладками браузера.

К примеру, мне потребовалось решить эту задачу при реализации JavaScript API к Comet сервису. Эта задача встречается достаточно часто и её уже рассматривали на хабре раньше здесь и здесь, но я решил написать своё решение задачи исходя из следующих требований к коду:

  • Кросбраузерность
  • Отсутствие зависимостей
  • Минимальный размер кода
  • Простота и удобство


Свою мини библиотеку я реализовал в стиле сигналов и слотов.

Эта очень удобная модель и мне кажется она в данном примере как нельзя лучше подходит. Достоинством этого подхода является слабая связность взаимодействующих между собой компонентов. Если кратко то модель сигналов и слотов нам даёт следующие возможности:

  • Код который излучает сигнал может ничего не знать о коде который этот сигнал обрабатывает. Он вообще не знает есть ли этот код или он вещает в пустоту;
  • Код принимающий сигнал не знает не чего об отправителе;
  • Единственное что является общим это формат сообщения.

Вот на пример нам надо оповестить все подписавшиеся функции о каком то событии.
Для этого выполняем:

tabSignal().emitAll('ИмяСобытия', "Данные") // Для уведомления всех открытых вкладок
tabSignal().emit( 'ИмяСобытия', "Данные" )  // Для работы в пределах одной вкладки

Всё код отработал и если был кто то подписан на это событие он получит данные.

Для подписки на событие надо передать имя события, на которое подписываемся и callBack для вызова на тот случай если событие произойдёт.

tabSignal().connect('ИмяСобытия',  function(param, signal_name){ });

Можно также передать ещё и имя слота, оно может понадобится если вы вдруг решили отписаться от уведомлений о событии.

tabSignal().connect("ИмяСобытия",'ИмяСлота', function(param, signal_name){} );

Здесь param будет содержать само сообщение. А signal_name имя сигнала, оно полезно на тот случай, если вы подписали один callBack на несколько разных сигналов

Вот код на тот случай если вам надо отписаться от события.

tabSignal().disconnect("ИмяСобытия", 'ИмяСлота');

Для передачи данных на другую вкладу библиотека просто пишет их в local storage браузера. Для того, чтобы получать данные, библиотека подписывается на событие onstorage, оно происходит во всех вкладках, когда кто-то пишет что-нибудь в local storage.

Я не стал обременять саму библиотеку функцией выбора мастер-вкладки, поэтому приведу её здесь. Заодно разберём алгоритм её работы. Но для начала расскажу, для чего вообще понадобилось искать мастер вкладку. Как уже говорил, я занимался разработкой JavaScript API к comet сервису.

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

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

К примеру, chrome открывает не более 6 запросов к одному домену и не более 255 запросов в сумме на все открытые вкладки — не важно, к какому из доменов. Соответственно, если поддерживать отдельное соединение с комет сервером на каждой вкладке, то сможете открыть не более 6 вкладок, а потом всё.

Соответственно, исходя из этой задачи я решил, что мастер вкладкой будет первая из открытых вкладок, а если её закроют, то мастером станет случайная из оставшихся. Для этого мастер вкладка отправляет сообщение всем вкладкам каждые 150 миллисекунд о том, что она вообще есть.

При открытии вкладки подписываемся на получение уведомлений от мастер вкладки.

Затем ставим таймер хотя бы на 300мс, и если за это время не получаем уведомления от мастера, то считаем, что мастера нет, и мы за него. В таком случаи начинаем рассылать уведомления о том, что мы мастер вкладка каждые 50мс, а если получили уведомление от мастер вкладки, то отменяем поставленный таймер и сразу ставим его обратно — и так до тех пор, пока мастер вкладка успевает напомнить о своём существовании за время, меньшее 300мс.

Ну а теперь реализация в коде:

function tryStartMasterTab(masterCallback, slaveCallback)
{ 
     var time_id = false;
     var last_time_id = false;
     var start_timer = 2000;
     
     if( window.InTryStartMasterTab != undefined )
     {
        console.log("Уеже запущено");
        return InTryStartMasterTab;
     }
    console.log("Запуск tryStartMasterTab");
     
     InTryStartMasterTab = 0;

    var setAsMaster = function(){
        // Отписываемся от уведомлений о наличии мастер вкладки
        tabSignal().disconnect("comet_msg_connect", 'comet_msg_master_signal');

        // Испускаем сигнал для уведомления всех остальных вкладок о своём превосходстве
        tabSignal().emitAll('comet_msg_master_signal')

        // Поставим таймер для уведомления всех остальных вкладок о своём превосходстве
        setInterval(function()
        {
            tabSignal().emitAll('comet_msg_master_signal')
            console.log("Мы мастер!");
        }, start_timer/8);

        InTryStartMasterTab = 1;
        if(masterCallback) masterCallback();
    }


     // Подключаемся на уведомления от других вкладок о том что уже есть мастер вкладка,
     // если за start_timer милисекунд уведомление произойдёт то отменим поставленный ранее таймер
     tabSignal().connect("comet_msg_connect",'comet_msg_master_signal', function()
     {
        if(time_id !== false) //  отменим поставленый ранее таймер если это ещё не сделано
        {
            console.log("Мы slave!, clearTimeout(time_id="+time_id+")");
            clearTimeout( time_id );
            time_id = setTimeout(setAsMaster, start_timer )
        }

         if(InTryStartMasterTab == 0)
         {
             if(slaveCallback) slaveCallback();
         }
         InTryStartMasterTab = -1;
     })
     // Создадим таймер, если этот таймер не будет отменён за start_timer милисекунд то считаем себя мастер вкладкой
     time_id = setTimeout(setAsMaster, start_timer )
}

В конце привожу online demo.

Репозиторий TabSignal.js.

Автор: Levhav

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js