Общение между вкладками браузера javascript
Мне захотелось наладить общение скриптов из разных вкладок браузера. Будущий API SharedWorker позволяет передавать данные между разными iframe и даже вкладками или окнами. В Chrome он работает давно, в Firefox – недавно, а в IE и Safari его не видать. Но существует кроссбраузерная альтернатива, о которой мало кто знает. Давайте разбираться.
Представьте, что на одной вкладке человек залогинился, затем открыл другую, и там разлогинился. На первой он вроде как залогинен, но когда он сделает там что-либо, ему выдадут ошибку. Хорошо было бы хотя бы показать ему диалог о том, что он разлогинился и ему надо войти ещё раз.
Можно было бы использовать API WebSocket, но это черезчур сложно. Я начал искать другие решения. Первое – сохранить куки и localStorage, и проверять их периодически. Но это нагружало бы процессор достаточно бесполезной задачей – ведь выхода могло и не случиться вообще. Меня больше устроили бы варианты с long-polling (длинные запросы), Server-Sent Events или WebSockets. Удивительно, но в результате оказалось, что ответ лежал в области localStorage!
Знаете ли вы, что localStorage запускают события? Точнее, событие возникает, когда нечто добавляется, меняется или удаляется из хранилища. Это значит, что когда вы касаетесь localStorage в любой вкладке, все остальные могут узнать об этом. Достаточно прослушивать события в объекте window.
У объекта event есть следующие свойства:
key – ключ, который трогали в localStorage
newValue – новое назначенное ему значение
oldValue – значение перед изменением
url – URL страницы, на которой случилось изменение
Поэтому можно наладить общение между вкладками, просто задавая значения в localStorage. Представьте следующий пример (псевдокод):
Когда пользователь выходит на одной из вкладок, и переходит на другую, страница перезагружается и серверная логика перенаправляет его куда-то. При этом проверка происходит только если пользователь выбрал эту вкладку. Если вдруг он вышел и вошёл на одной из вкладок, нет нужды разлогинивать его на всех остальных.
Это должно работать и в другую сторону – если пользователь залогинился на одной вкладке, а на другой он был разлогинен, то при переходе на вторую вкладку страница перезагрузится и он там тоже будет залогинен. Радость.
API попроще
localStorage API — один из самых простых интерфейсов. Однако и у него есть особенность – например Safari и QuotaExceededError, нет поддержки JSON и старых браузеров.
Для этого я сделал модуль, предоставляющий локальное хранилище с упрощённым API, избавляющий вас от указанных особенностей, работающий с памятью, если вдруг поддержки localStorage нет, и позволяющий проще работать с событиями. Можно регистрировать и удалять слушателей этих событий для заданных ключей.
Вот схема работы с local-storage.
Стоит упомянуть, что local-storage регистрирует один обработчик событий и отслеживает все ключи, за которыми вы наблюдаете, вместо регистрации множества событий.
Наверно можно придумать и другие случаи, когда возможно использовать общение между вкладками. Пока SharedWorker не получил должного распространения, а подход WebSockets ненадёжен, если ваше приложение ориентируется в первую очередь на работу оффлайн.
Давным-давно в далёкой галактике появилась задача по синхронизации вкладок браузера для веб-плеера, наподобие VK: нужно было организовать обмен данными между вкладками, отслеживать их количество и назначать задачи некоторым из них. Всю реализацию нужно было выполнить на клиенте. Информации собрано много, и набралось на целую статью.
Ниже опишу различные способы решения подобных задач.
Рассмотрим наиболее популярные способы синхронизации вкладок браузера в порядке увеличения сложности.
Local Storage
localStorage – локальное хранилище, свойство объекта window, позволяет получить доступ к локальному Storage объекту. В нем можно хранить данные между сессиями пользователя. Есть аналогичное свойство – sessionStorage, но оно хранит данные только в течение сессии страницы.
Данные в storage добавляются с помощью метода setItem.
Событие storage идеально подходит для синхронизации данных между вкладками, оно генерируется при изменении значения элемента localStorage или sessionStorage.
Событие не работает на вкладке, которая вносит изменения, но срабатывает на остальных вкладках домена в браузере.
Генерация события storage
Браузеры имеют различный уровень объема хранилищ для localStorage и sessionStorage:
- Chrome, FireFox и Opera ~ 5 МБ.
- IE ~ 4,8 МБ.
- iOS Safari, OS X Safari ~ 2,5 МБ.
- Android ~ 5 МБ.
Post Message
Передаваемые данные могут быть любым объектом, который поддерживает клонирование (строка, объект, массив, Map, Date . ). Но IE поддерживает только строки.
В браузере IE интерфейс postMessage работает только с iframes и не работает между вкладками и окнами.
Broadcast Channel API
На первый взгляд можно найти несколько похожих методов передачи данных (например MessageChannel, WebSocket), но каждый из них служит определенной цели — их сравнение.
Web Workers
Воркеры отлично подходят для того, чтобы выполнять тяжелые вычислительные операции, не замедляя работу пользовательского интерфейса.
Но с синхронизацией помочь могут только два вида воркеров.
Shared Worker
Это особый вид воркера, к которому можно получить доступ из нескольких контекстов браузера. Напишем общий js-файл для вкладок, например shared-worker.js.
Каждая вкладка может связываться с воркером через worker.port. Скрипт воркера также имеет доступ к своим портам. Каждый раз, когда вкладка подключается к воркеру, в сценарии запускается событие connect.
Метод postMessage создан для отправки данных вкладки на общий воркер.
Получить данные воркера можно с помощью события message.
В SharedWorker API есть событие connect, но нет события disconnect, и поэтому данные не смогут самоочищаться в закрытых вкладках — они будут продолжать считаться открытыми. Это не приведет к ошибкам, но можно считать это недоработкой или фичей API.
Работает только в Chrome и FF.
Service Worker
Подробное использование и множество примеров тут.
У всех веб-воркеров нет доступа к объектам window и document.
Service worker не работает в IE и Opera mini.
Библиотеки синхронизации
Это способ для тех, кто не хочет велосипедить и готов рассмотреть уже имеющиеся решения.
-
, использует Сookie и Local Storage; , использует SharedWorker; , использует Page Visibility API; , использует Local Storage и WebSocket; , использует Worker и localStorage;
Чтобы подвести окончательные итоги, сравним методы по поддержке браузерами наглядно.
Для управления блокировками совместного состояния и совместных файлов наиболее подходящим решением является Shared Workers и Service Worker.
А для задачи с веб-плеером был выбран LocalStorage, так как есть поддержка IE.
Надеюсь, что статья помогла вам в выборе подходящего способа синхронизации.
Существуют стандартные решения вроде BroadcastChannel, однако поддержка в браузерах сейчас оставляет желать лучшего, поэтому мы решили реализовать свою библиотеку. Когда библиотека была готова, выяснилось, что такая функциональность уже не нужна, зато появилась другая задача: нужно было общаться между iframe и основным окном.
При ближайшем рассмотрении выяснилось, что две трети библиотеки при этом можно не менять, необходимо только немного порефакторить код. Библиотека представляет из себя скорей ПРОТОКОЛ общения, который может работать с текстовыми данными. Его можно применять во всех случаях, если есть возможность передавать текст (iframe, window.open, worker, вкладки браузера, WebSocket).
Как это работает
- type — тип события 0
- name — наименование события пользователя
- data — данные пользователя (JSON-like).
Схема работы с событиями:
Отправка запроса
Отправка запроса подразумевает, что внутри библиотеки формируется ID запроса, библиотека будет ожидать ответа с данным ID, и после успешного ответа из него будут удалены служебные поля, а ответ вернется пользователю. Кроме того, можно установить максимальное время ожидания ответа.
С запросом все обстоит несколько сложнее. Чтобы ответить на запрос, необходимо объявить методы, которые доступны в нашем протоколе. Это делается с помощью метода registerRequestHandler. Он принимает имя запроса, на который будет отвечать, и функцию, которая возвращает ответ. Для создания запроса нам нужен id, и в общем-то можно использовать timestamp, но это очень не удобно отлаживать. Поэтому это id класса который отправляет запрос + порядковый номер запроса + строковая константа. Далее мы конструируем объект с полями id, type — со значением 1, name — наименование запроса, data — данные пользователя (JSON-like).
При получении запроса мы проверяем, есть ли у нас API для ответа на данный запрос, если API нет — возвращаем ошибку. Если API есть — возвращаем результат выполнения функции из registerRequestHandler, с соответствующим именем запроса.
Адаптеры. Реализация протокола.
Небольшие тонкости, с которыми мы столкнулись:
Примеры использования:
Библиотеку можно установить с помощью своего любимого пакетного менеджера — @waves/waves-browser-bus
Чтобы установить двустороннюю связь с iframe, достаточно написать код:
И внутри iframe:
Что дальше?
Получился гибкий и универсальный протокол, который можно использовать в любой ситуации.
Теперь я планирую отделить адаптеры от протокола и вынести их в отдельные npm-пакеты, добавить адаптеры для работы с worker и вкладками браузера. Хочется, чтобы писать адаптеры, реализующие протокол для любых других нужд, было максимально просто.
Если у вас есть желание присоединиться к разработке или идеи по функционалу библиотеки — милости прошу в репозиторий.
Политика «Одинакового источника» (Same Origin) ограничивает доступ окон и фреймов друг к другу.
Политика "Одинакового источника"
Два URL имеют «одинаковый источник» в том случае, если они имеют совпадающие протокол, домен и порт.
Эти URL имеют одинаковый источник:
А эти – разные источники:
Политика «Одинакового источника» говорит, что:
- если у нас есть ссылка на другой объект window , например, на всплывающее окно, созданное с помощью window.open или на window из и у этого окна тот же источник, то к нему будет полный доступ.
- в противном случае, если у него другой источник, мы не сможем обращаться к его переменным, объекту document и так далее. Единственное исключение – объект location : его можно изменять (таким образом перенаправляя пользователя). Но нельзя читать location (нельзя узнать, где находится пользователь, чтобы не было никаких утечек информации).
Доступ к содержимому ифрейма
Внутри находится по сути отдельное окно с собственными объектами document и window .
Мы можем обращаться к ним, используя свойства:
- iframe.contentWindow ссылка на объект window внутри .
- iframe.contentDocument – ссылка на объект document внутри , короткая запись для iframe.contentWindow.document .
Когда мы обращаемся к встроенному в ифрейм окну, браузер проверяет, имеет ли ифрейм тот же источник. Если это не так, тогда доступ будет запрещён (разрешена лишь запись в location , это исключение).
Для примера давайте попробуем чтение и запись в ифрейм с другим источником:
Код выше выведет ошибку для любых операций, кроме:
- Получения ссылки на внутренний объект window из iframe.contentWindow
- Изменения location .
С другой стороны, если у ифрейма тот же источник, то с ним можно делать всё, что угодно:
Событие iframe.onload – по сути то же, что и iframe.contentWindow.onload . Оно сработает, когда встроенное окно полностью загрузится со всеми ресурсами.
…Но iframe.onload всегда доступно извне ифрейма, в то время как доступ к iframe.contentWindow.onload разрешён только из окна с тем же источником.
Окна на поддоменах: document.domain
По определению, если у двух URL разный домен, то у них разный источник.
Для этого в каждом таком окне нужно запустить:
После этого они смогут взаимодействовать без ограничений. Ещё раз заметим, что это доступно только для страниц с одинаковым доменом второго уровня.
Ифрейм: подождите документ
Когда ифрейм – с того же источника, мы имеем доступ к документу в нём. Но есть подвох. Не связанный с кросс-доменными особенностями, но достаточно важный, чтобы о нём знать.
Когда ифрейм создан, в нём сразу есть документ. Но этот документ – другой, не тот, который в него будет загружен!
Так что если мы тут же сделаем что-то с этим документом, то наши изменения, скорее всего, пропадут.
Нам не следует работать с документом ещё не загруженного ифрейма, так как это не тот документ. Если мы поставим на него обработчики событий – они будут проигнорированы.
Как поймать момент, когда появится правильный документ?
Можно проверять через setInterval :
Коллекция window.frames
Другой способ получить объект window из – забрать его из именованной коллекции window.frames :
- По номеру: window.frames[0] – объект window для первого фрейма в документе.
- По имени: window.frames.iframeName – объект window для фрейма со свойством name="iframeName" .
Ифрейм может иметь другие ифреймы внутри. Таким образом, объекты window создают иерархию.
Навигация по ним выглядит так:
- window.frames – коллекция «дочерних» window (для вложенных фреймов).
- window.parent – ссылка на «родительский» (внешний) window .
- window.top – ссылка на самого верхнего родителя.
Можно использовать свойство top , чтобы проверять, открыт ли текущий документ внутри ифрейма или нет:
Атрибут ифрейма sandbox
Атрибут sandbox позволяет наложить ограничения на действия внутри , чтобы предотвратить выполнение ненадёжного кода. Атрибут помещает ифрейм в «песочницу», отмечая его как имеющий другой источник и/или накладывая на него дополнительные ограничения.
Существует список «по умолчанию» ограничений, которые накладываются на . Их можно уменьшить, если указать в атрибуте список исключений (специальными ключевыми словами), которые не нужно применять, например: .
Другими словами, если у атрибута "sandbox" нет значения, то браузер применяет максимум ограничений, но через пробел можно указать те из них, которые мы не хотим применять.
Вот список ограничений:
allow-same-origin "sandbox" принудительно устанавливает «другой источник» для ифрейма. Другими словами, он заставляет браузер воспринимать iframe , как пришедший из другого источника, даже если src содержит тот же сайт. Со всеми сопутствующими ограничениями для скриптов. Эта опция отключает это ограничение. allow-top-navigation Позволяет ифрейму менять parent.location . allow-forms Позволяет отправлять формы из ифрейма. allow-scripts Позволяет запускать скрипты из ифрейма. allow-popups Позволяет открывать всплывающие окна из ифрейма с помощью window.open .
Больше опций можно найти в справочнике.
Пример ниже демонстрирует ифрейм, помещённый в песочницу со стандартным набором ограничений: . На странице содержится JavaScript и форма.
Обратите внимание, что ничего не работает. Таким образом, набор ограничений по умолчанию очень строгий:
На протяжении многих лет возможности браузеров постоянно увеличивались в ответ на растущие потребности веб-приложений. И теперь у нас есть множество способов получения одной и той же (или схожей) функциональности. На такую особенность браузеров, как возможность обмена данными между вкладками, редко обращают внимание. Рассмотрим несколько сценариев, в которых она может потребоваться:
- Изменение темы (например, тёмная или светлая тема) приложения распространяется на уже открытые вкладки браузера.
- Получение последнего токена для аутентификации и использование его во всех вкладках браузера.
- Синхронизация состояния приложения во всех вкладках браузера.
На момент написания этой статьи существовало несколько достойных внимания подходов, в соответствии с которыми осуществляется обмен данными между вкладками браузеров. У каждого из этих подходов свои сильные и слабые стороны, поэтому остановимся на них подробнее, чтобы вы могли найти подходящий для вас вариант.
Возможно, вы уже использовали LocalStorage , который доступен на разных вкладках в рамках одного и того же приложения-источника. Но знаете ли вы, что LocalStorage поддерживает события? Эту функцию можно использовать для обмена данными между вкладками браузера: хранилище обновляется, после чего событие получат другие вкладки.
Например, в одной вкладке выполняется следующий код JavaScript:
И другие, прослушивающие событие вкладки получат это событие:
Но здесь есть несколько ограничений:
- Событие не срабатывает для вкладки, на которой выполняется действие по вводу значений в хранилище.
- Этот подход имеет негативные последствия для большого объёма данных: из-за синхронности выполняемых в LocalStorage действий основной поток пользовательского интерфейса может быть заблокирован.
API широковещательного канала позволяет осуществлять обмен данными между вкладками, окнами, фреймами, Iframes и веб-воркерами. Одна вкладка создаёт что-то и опубликовывает это на канале:
А другие вкладки прослушивают канал:
И таким образом происходит обмен данными между контекстами браузера (окнами, вкладками, фреймами или Iframes). Такой способ обмена данными между вкладками браузера очень удобен. Тем не менее safari и IE его не поддерживают. Более подробная информация содержится в разделе «BroadcastChannel» документации MDN.
Подсказка: выкладывайте на Bit (Github) переиспользуемые компоненты для своих проектов. Здесь очень просто размещать, документировать и организовывать независимые компоненты из любого проекта.
Задействуйте этот ресурс для максимально многократного использования кода и совместной работы над независимыми компонентами, а также для создания масштабируемых приложений.
Bit поддерживает Node, TypeScript, React, Vue, Angular и много других инструментов.
А принимающий воркер на другой вкладке браузера прослушивает это событие:
А целевое окно прослушивает события:
Одно из преимуществ этого подхода перед другими — возможность поддержки обмена данными между разными источниками. Но есть и ограничение: необходима ссылка на другую вкладку браузера. Поэтому этот подход только для вкладок браузера, открытых через window.open() или document.open() . Более подробная информация содержится в документации MDN.
Надеюсь, статья была для познавательной и рассмотренные в ней подходы будут полезны для ваших веб-приложений. Каждый подход уникален и имеет свои варианты использования.
Кроме этих четырёх подходов, для обмена данными между вкладками браузера и даже между устройствами в режиме реального времени применяются Websockets («веб-сокеты») и Server-Sent Events («события, посылаемые сервером»). Но для этого понадобится веб-сервер. А вот рассмотренные в статье подходы не зависят от веб-сервера, и их применение позволяет осуществлять обмен данными в браузере без него и делать это быстро.
Читайте также: