Каждое окно и каждая вкладка браузера — это отдельный, изолированный мир. У них свой объект window, свой JavaScript-контекст, и напрямую дотянуться из одного до переменных другого нельзя — иначе любой открытый сайт читал бы данные соседней вкладки. Но задача «передать что-то из одного окна в другое» встречается постоянно: авторизация в попапе, который должен вернуть токен на основную страницу; виджет в отдельном окне; синхронизация корзины между вкладками интернет-магазина.
Для этого в браузере есть несколько штатных каналов связи. Разберём четыре подхода — от классической пары «окно + попап» до широковещательной рассылки на все вкладки сайта — и в конце сведём их в таблицу, чтобы было понятно, что под какую задачу брать.
Открываем окно и держим на него ссылку
Начнём с самого прямого сценария: главная страница открывает дочернее окно и общается с ним. Метод window.open() не просто открывает попап — он возвращает ссылку на его объект window. Эту ссылку и нужно сохранить, через неё пойдёт вся связь.
let popup;
function openPopup() {
const options = "width=360,height=420,resizable=no,menubar=no,toolbar=no";
popup = window.open("/widget.html", "widget", options);
}
Третий аргумент — строка с параметрами окна (размеры, наличие тулбаров) без пробелов, через запятую. Второй аргумент — имя окна: если открыть open() с тем же именем ещё раз, браузер переиспользует уже открытое окно, а не создаст новое.
Обратная ссылка тоже есть: у дочернего окна свойство window.opener указывает на то окно, которое его открыло. Так что связь потенциально двусторонняя — родитель знает про ребёнка через переменную popup, ребёнок про родителя через opener.
postMessage: двусторонний канал
Иметь ссылку на чужое окно — не значит читать его переменные напрямую (браузер это запретит для разных источников). Легальный способ обмена — метод postMessage(): одно окно отправляет данные, другое ловит их событием message.
Отправляем из главного окна в попап:
function greetPopup() {
popup.postMessage(
{ type: "greet", text: "Привет из главного окна" },
window.location.origin
);
}
Передавать можно не только строки: аргумент клонируется по алгоритму структурного клонирования, поэтому спокойно уходят объекты, массивы, Map, Date и прочее (кроме функций и DOM-узлов).
Ловим сообщение на принимающей стороне — это работает одинаково и в попапе, и в родителе, обмен данными между окнами вообще идёт через одно и то же событие message объекта window:
window.addEventListener("message", (event) => {
if (event.data?.type === "greet") {
console.log(event.data.text);
}
});
Из попапа назад в родителя отправляем через opener — и, например, тут же закрываем окно:
function reply(text) {
window.opener.postMessage({ type: "reply", text }, window.location.origin);
window.close();
}
Поле type в объекте — не требование API, а удобная привычка: на один и тот же слушатель обычно прилетают разные сообщения, и по типу их проще разбирать.
Не ставь звёздочку: targetOrigin и проверка origin
Второй аргумент postMessage() — это targetOrigin, то есть источник, которому сообщение адресовано. В примерах из интернета там часто стоит "*" — «отправить кому угодно». Это удобно и опасно: если пользователь перейдёт в попапе на чужой сайт, сообщение с вашими данными уедет ему.
Правильно — указывать конкретный источник. Тогда браузер доставит сообщение, только если окно действительно на нём находится:
// уедет, только если popup сейчас на нашем origin
popup.postMessage(payload, "https://example.com");
// на своём же сайте удобно так:
popup.postMessage(payload, window.location.origin);
Симметрично — на приёме. Событие message может прийти откуда угодно, в том числе от постороннего окна, поэтому первым делом проверяем event.origin и игнорируем чужаков:
window.addEventListener("message", (event) => {
if (event.origin !== window.location.origin) return; // чужой источник
handle(event.data);
});
Помимо origin и data, у события есть event.source — ссылка на окно-отправитель. Через неё удобно ответить, не заводя отдельную переменную: event.source.postMessage(...).
Вкладки одного сайта: BroadcastChannel
Подход с opener хорош, когда одно окно открыло другое и держит на него ссылку. Но если у пользователя просто открыто три вкладки вашего магазина, прямых ссылок между ними нет — и передать «в корзину добавили товар» сразу во все становится неудобно.
Для этого случая есть BroadcastChannel — общая «радиостанция» для всех вкладок, окон и даже воркеров одного источника. Кто подписался на канал с тем же именем — тот и слышит сообщения. Никаких ссылок на конкретные окна не нужно.
const channel = new BroadcastChannel("cart");
// одна вкладка положила товар и вещает об этом
channel.postMessage({ type: "add", id: 42 });
// все остальные вкладки слушают
channel.onmessage = (event) => {
if (event.data.type === "add") {
updateCartBadge(event.data.id);
}
};
// когда канал больше не нужен
channel.close();
Важная деталь: отправитель своё сообщение не получает — onmessage срабатывает только у других подписчиков канала. Это ровно то, что нужно для синхронизации: вкладка, которая уже обновила своё состояние, повторно его не обрабатывает.
Классика: событие storage
Ещё до BroadcastChannel вкладки синхронизировали через localStorage. Приём такой: одна вкладка что-то пишет в хранилище, а во всех остальных вкладках того же источника срабатывает событие storage.
// в любой вкладке меняем значение
localStorage.setItem("theme", "dark");
// во всех прочих вкладках прилетит событие
window.addEventListener("storage", (event) => {
if (event.key === "theme") {
applyTheme(event.newValue); // "dark"
}
});
Здесь та же логика «не услышу сам себя»: событие storage не срабатывает в той вкладке, которая сделала запись, — только в соседних. На этом часто спотыкаются, когда пытаются проверить обработчик в одной вкладке и удивляются, что он молчит.
У этого способа есть плюс, которого нет у остальных: раз данные лежат в localStorage, они переживают перезагрузку и доступны вкладке, которую откроют потом. Минус — передавать так можно только то, что сериализуется в строку, и ради чистого обмена сообщениями (без сохранения) тащить данные в хранилище уже незачем — для этого есть BroadcastChannel. О том, какие вообще бывают хранилища в браузере и чем они отличаются, есть отдельный разбор.
Что выбрать
Короткая шпаргалка по четырём каналам:
| Способ | Когда подходит | Разные источники? |
| postMessage + opener | Окно открыло попап или встроило iframe и держит на него ссылку | Да, для того и создан |
| BroadcastChannel | Несколько вкладок/воркеров одного сайта, прямых ссылок между ними нет | Нет, только один источник |
| Событие storage | Синхронизация вкладок + нужно, чтобы данные пережили перезагрузку | Нет, только один источник |
Практическое правило: если общаются окна разных сайтов (попап авторизации, сторонний iframe) — это только postMessage с обязательной проверкой origin. Если это вкладки одного вашего сайта — берите BroadcastChannel, а storage оставьте на случай, когда обмен нужно совместить с сохранением состояния.
Итог
Изоляция окон — это защита, а не преграда: браузер даёт штатные каналы, чтобы пробить её осознанно. Между окном и его попапом/iframe работает postMessage — главное не лениться и указывать конкретный targetOrigin, а на приёме проверять event.origin. Для вкладок одного сайта проще и чище BroadcastChannel, а старое доброе событие storage выручает, когда обмен нужно совместить с сохранением данных.
Комментарии (0)