Это первая статья в нашем корпоративном блоге. На этот раз я расскажу о нашем решении задачи обмена сообщениями между вкладками браузера.
К примеру, мне потребовалось решить эту задачу при реализации 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