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