W3docs

Межоконная коммуникация в JavaScript

Межоконная коммуникация в JavaScript: postMessage, политика одного источника, ссылки на окна, события localStorage и Broadcast Channel API с примерами.

Межоконная коммуникация — это обмен данными между отдельными контекстами браузера: родительской страницей и открытым ею всплывающим окном, страницей и встроенным iframe или двумя вкладками одного сайта. Браузер намеренно изолирует эти контексты из соображений безопасности, поэтому они не могут свободно читать переменные или DOM друг друга. Вместо этого JavaScript предоставляет небольшой набор чётко определённых каналов для передачи сообщений между ними.

В этой главе рассматривается, когда нужна межоконная коммуникация, политика одного источника (same-origin policy), которая её регулирует, и четыре практических механизма: postMessage(), прямые ссылки на окна, события хранилища и Broadcast Channel API. Она опирается на материал главы window.open() и всплывающие окна; если вы только знакомитесь с моделью браузера, начните с обзора среды браузера.

Понимание межоконной коммуникации

Контекст браузера — это любой объект, имеющий собственный объект window: вкладка, всплывающее окно или iframe. Два контекста могут взаимодействовать только через управляемый API, и допустимый объём взаимодействия зависит от их источника (origin) — комбинации протокола, хоста и порта (например, https://www.w3docs.com:443).

Политика одного источника

Политика одного источника (Same-Origin Policy, SOP) — это правило, определяющее, что один контекст может делать с другим:

  • Один источник (одинаковые протокол, хост и порт): контексты могут напрямую читать DOM друг друга и свободно вызывать postMessage().
  • Разные источники (любая часть отличается): прямой доступ к DOM заблокирован. Единственным допустимым каналом является postMessage(), который получатель обязан валидировать.

Именно поэтому postMessage() — рекомендуемый подход практически везде: он работает одинаково вне зависимости от того, имеют ли окна общий источник или нет, и заставляет явно указывать, кому вы доверяете.

Когда это нужно

  1. Всплывающие окна. Окно, открытое с помощью window.open(), часто должно отправить результаты обратно на страницу, которая его открыла (всплывающее окно авторизации OAuth, выбор файла).
  2. Iframe. Встроенные виджеты — платёжные формы, карты, сторонние плееры — обмениваются данными с хост-страницей.
  3. Вкладки и другие контексты. Две вкладки одного приложения могут нуждаться в синхронизации (выход из системы в одной вкладке должен выполнить выход в остальных).

Методы межоконной коммуникации

Использование window.postMessage()

Метод window.postMessage() — самый безопасный и переносимый способ отправки данных между окнами или фреймами: он работает как для одного источника, так и для разных. Отправитель вызывает targetWindow.postMessage(data, targetOrigin), а получатель слушает событие message.

Два аргумента важны с точки зрения безопасности:

  • targetOrigin (второй аргумент postMessage) ограничивает тех, кто может получить сообщение. Передавайте точный ожидаемый источник ('https://example.com'); используйте подстановочный символ '*' только если данные не являются чувствительными, поскольку в этом случае любое окно на этом целевом адресе сможет их прочитать.
  • event.origin (у получателя) сообщает кто отправил сообщение. Всегда проверяйте его перед тем, как доверять event.data — именно так вы отклоняете сообщения от ненадёжных страниц.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Cross-Window Communication</title>
  <style>
    #childIframe, #childPopup {
      width: 100%;
      height: 200px;
      border: 1px solid black;
      margin-top: 20px;
    }
  </style>
</head>
<body>
  <h1>Cross-Window Communication Examples</h1>

  <!-- Button to Open Popup -->
  <button id="openPopup">Open Popup</button>
  <div id="parentPopupDisplay"></div>

  <!-- Iframe -->
  <iframe id="childIframe" srcdoc="
    <!DOCTYPE html>
    <html lang='en'>
    <head>
      <meta charset='UTF-8' />
      <title>Child Iframe</title>
    </head>
    <body>
      <div id='childIframeDisplay'></div>
      <script>
        window.addEventListener('message', (event) => {
          // Note: For cross-origin contexts, replace window.location.origin with the hardcoded parent origin.
          if (event.origin !== window.location.origin) return;
          document.getElementById('childIframeDisplay').innerText = 'Message from parent: ' + event.data;
          event.source.postMessage('Hello, Parent Window!', event.origin);
        });
      </script>
    </body>
    </html>
  "></iframe>
  <div id="iframeDisplay"></div>

  <!-- Scripts for Parent Window -->
  <script>
    // Handle Popup Communication
    document.getElementById('openPopup').addEventListener('click', () => {
      const popup = window.open('', 'popupWindow', 'width=600,height=400');
      popup.document.write(`
        <!DOCTYPE html>
        <html lang='en'>
        <head>
          <meta charset='UTF-8' />
          <title>Popup Window</title>
        </head>
        <body>
          <div id='popupDisplay'></div>
          <script>
            window.addEventListener('message', (event) => {
              // Note: For cross-origin contexts, replace window.location.origin with the hardcoded parent origin.
              if (event.origin !== window.location.origin) return;
              document.getElementById('popupDisplay').innerText = 'Message from parent: ' + event.data;
              event.source.postMessage('Hello, Parent Window!', event.origin);
            });
          <\/script>
        </body>
        </html>
      `);
      setTimeout(() => {
        // For cross-origin, replace '*' with the exact target origin (e.g., 'https://example.com')
        popup.postMessage('Hello from parent!', '*');
      }, 1000);
    });

    // Handle Iframe Communication
    const iframe = document.getElementById('childIframe');

    iframe.onload = () => {
      iframe.contentWindow.postMessage('Hello from parent window!', '*');
    };

    window.addEventListener('message', (event) => {
      if (event.origin !== window.location.origin) return;
      if (event.source === iframe.contentWindow) {
        document.getElementById('iframeDisplay').innerText = 'Message from iframe: ' + event.data;
      } else {
        document.getElementById('parentPopupDisplay').innerText = 'Message from popup: ' + event.data;
      }
    });
  </script>
</body>
</html>

В этом комплексном примере родительское окно открывает всплывающее окно и встраивает iframe. Как всплывающее окно, так и iframe могут общаться с родительским окном через postMessage(). Сообщения отображаются внутри соответствующих элементов div для удобной визуализации.

Примечание

Хотя document.write() подходит для простых демонстраций, современные лучшие практики рекомендуют использовать DOMParser или Blob URL для безопасной вставки содержимого во всплывающие окна.

Доступ к ссылкам на окна

Когда вы открываете новое окно с помощью window.open(), возвращаемое значение является ссылкой на это окно. Открытое окно, в свою очередь, может обращаться к открывшему его окну через window.opener, а родитель может обращаться к iframe через iframe.contentWindow. Эти прямые ссылки работают только в том случае, если оба контекста имеют одинаковый источник — в противном случае SOP выбрасывает ошибку безопасности. Используйте их для тесно связанных страниц с одним источником; прибегайте к postMessage() всякий раз, когда задействована граница источников.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Direct Manipulation Example</title>
  <style>
    #childIframe {
      width: 100%;
      height: 200px;
      border: 1px solid black;
      margin-top: 20px;
    }
  </style>
</head>
<body>
  <h1>Direct Manipulation Example</h1>

  <!-- Button to Open Popup -->
  <button id="openChild">Open Child Window</button>
  <div id="parentChildDisplay"></div>

  <!-- Iframe -->
  <iframe id="childIframe" srcdoc="
    <!DOCTYPE html>
    <html lang='en'>
    <head>
      <meta charset='UTF-8' />
      <title>Child Iframe</title>
    </head>
    <body>
      <div id='childIframeContent'>Initial Content</div>
    </body>
    </html>
  "></iframe>

  <!-- Scripts for Parent Window -->
  <script>
    document.getElementById('openChild').addEventListener('click', () => {
      const childWindow = window.open('', 'childWindow', 'width=600,height=400');
      childWindow.document.write(`
        <!DOCTYPE html>
        <html lang='en'>
        <head>
          <meta charset='UTF-8' />
          <title>Child Window</title>
        </head>
        <body>
          <div id='childContent'>Initial Content</div>
        </body>
        </html>
      `);
      // Ensure the content is updated after the window has fully loaded
      setTimeout(() => {
        childWindow.document.body.innerHTML += '<p>Message from parent window</p>';
      }, 1000); // Adjust the timeout duration as necessary
    });

    const iframe = document.getElementById('childIframe');

    iframe.onload = () => {
      const iframeDoc = iframe.contentWindow.document;
      iframeDoc.getElementById('childIframeContent').innerText += ' - Updated by Parent Window';
    };
  </script>
</body>
</html>

В этом примере родительское окно открывает дочернее окно и напрямую изменяет его содержимое после загрузки. Кроме того, оно обновляет содержимое встроенного iframe.

Внимание

Прямое манипулирование DOM через contentWindow.document или window.opener ограничено политикой одного источника (SOP) для контекстов с разными источниками. Для безопасной и надёжной коммуникации всегда отдавайте предпочтение postMessage(). Для всплывающих окон с одним источником window.opener можно использовать как альтернативу для прямого доступа к родительскому окну.

Примечание

При использовании srcdoc содержимое iframe загружается асинхронно. Обработчик onload гарантирует готовность DOM, но для сложных сценариев рассмотрите возможность запуска коммуникации через событие DOMContentLoaded, диспетчеризованное изнутри iframe.

Использование localStorage и sessionStorage

localStorage совместно используется всеми вкладками и окнами с одним источником, и запись в него запускает событие storage во всех остальных контекстах. Это делает его простым способом транслировать изменения между вкладками без каких-либо прямых ссылок на окна. (sessionStorage привязан к вкладке и не распространяется, поэтому он не подходит для межвкладочных сообщений.) Подробнее об объектах хранилища см. в разделе localStorage и sessionStorage.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Local Storage Example</title>
</head>
<body>
  <h1>Local Storage Example</h1>
  <button id="storeData">Store Data</button>
  <button id="retrieveData">Retrieve Data</button>
  <div id="storageDisplay"></div>

  <script>
    // Listen for changes triggered by other windows/tabs
    window.addEventListener('storage', (event) => {
      if (event.key === 'sharedData') {
        document.getElementById('storageDisplay').innerText = 'Updated Data: ' + event.newValue;
      }
    });

    document.getElementById('storeData').addEventListener('click', () => {
      localStorage.setItem('sharedData', 'This is shared data');
    });

    document.getElementById('retrieveData').addEventListener('click', () => {
      const data = localStorage.getItem('sharedData');
      document.getElementById('storageDisplay').innerText = 'Stored Data: ' + data;
    });
  </script>
</body>
</html>

В этом примере родительское окно сохраняет данные в localStorage и извлекает их при нажатии кнопок. Для обеспечения межоконной синхронизации добавлен слушатель события storage. Обратите внимание, что событие storage срабатывает только в других контекстах браузера, но не в том, который инициировал изменение.

Broadcast Channel API

Broadcast Channel API — это специально разработанный инструмент для обмена сообщениями между вкладками, окнами и iframe с одним источником. Любой контекст, открывающий канал с одинаковым именем, получает все сообщения, отправленные в него, — без ссылок на окна и без обходных решений через события storage. Он не может пересекать источники, поэтому для сторонних iframe по-прежнему нужен postMessage().

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Broadcast Channel Example</title>
</head>
<body>
  <h1>Broadcast Channel Example</h1>
  <button id="sendMessage">Send Message</button>
  <div id="broadcastDisplay"></div>

  <script>
    const channel = new BroadcastChannel('example_channel');

    channel.onmessage = (event) => {
      document.getElementById('broadcastDisplay').innerText = 'Broadcast message received: ' + event.data;
    };

    document.getElementById('sendMessage').addEventListener('click', () => {
      channel.postMessage('Hello from another context!');
    });
  </script>
</body>
</html>

В этом примере создаётся Broadcast Channel и при нажатии кнопки отправляется сообщение. Сообщение принимается и отображается внутри элемента div.

Для корректного тестирования этого примера:

  1. Нажмите кнопку «Try it Yourself» дважды, чтобы открыть страницу примера в двух разных вкладках.
  2. Затем нажмите кнопку «Send Message» в одной из вкладок/окон.
  3. Вы должны увидеть сообщение в другой вкладке/окне.

BroadcastChannel API предназначен для межвкладочного взаимодействия, поэтому сообщение будет отправлено из одной вкладки/окна во все остальные, открытые с тем же источником (в данном случае — тот же HTML-файл).

Выбор подходящего метода

МетодРазные источники?Лучше всего для
postMessage()ДаПо умолчанию. Всплывающие окна и сторонние iframe, везде, где присутствует граница источников.
Прямые ссылки на окнаНет (только один источник)Тесно связанные всплывающие окна/iframe с одним источником, которые вы полностью контролируете.
Событие storageНет (только один источник)Трансляция изменений состояния в другие вкладки без дополнительного API.
Broadcast ChannelНет (только один источник)Чистый обмен сообщениями «многие ко многим» между вкладками и фреймами с одним источником.

Если сомневаетесь, используйте postMessage() — это единственный метод, работающий между разными источниками, и единственный со встроенной моделью безопасности.

Лучшие практики

Сериализуйте сложные данные как JSON. postMessage() использует алгоритм структурированного клонирования и может передавать объекты напрямую, но явный JSON делает контракт понятным и работает с событиями storage (которые несут только строки):

const message = { type: 'greeting', content: 'Hello, Child Window!' };
// JSON.stringify produces: {"type":"greeting","content":"Hello, Child Window!"}
childWindow.postMessage(JSON.stringify(message), '*');

Обрабатывайте закрытые или недоступные цели. Всплывающее окно может быть закрыто пользователем, а окно с другим источником выбросит ошибку при прямом обращении. Защищайте свои вызовы:

if (childWindow && !childWindow.closed) {
  try {
    childWindow.postMessage('Hello, Child Window!', '*');
  } catch (e) {
    console.error('Failed to send message:', e);
  }
}

Всегда валидируйте отправителя. На стороне получателя проверяйте event.origin по списку разрешённых адресов перед обработкой event.data, и никогда не выполняйте eval() для входящего сообщения.

Заключение

Межоконная коммуникация в JavaScript — это мощная возможность, которая при правильном использовании может значительно повысить интерактивность и удобство работы с веб-приложениями. Используя такие методы, как window.postMessage(), локальное хранилище и Broadcast Channel API, разработчики могут эффективно управлять обменом данными между разными окнами, вкладками и фреймами. Следуйте лучшим практикам для обеспечения безопасной и надёжной коммуникации и применяйте приведённые примеры для интеграции этих техник в свои проекты.

Практика

Практика
Что верно относительно межоконной коммуникации в JavaScript?
Что верно относительно межоконной коммуникации в JavaScript?
Was this page helpful?