1 августа 2019 г.

Общение между окнами и фреймами

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Более новая информация по этой теме находится на странице https://learn.javascript.ru/cross-window-communication.

Элемент iframe является обычным узлом DOM, как и любой другой. Существенное отличие – в том, что с ним связан объект window внутреннего окна. Он доступен по ссылке iframe.contentWindow.

Таким образом, iframe.contentWindow.document будет внутренним документом, iframe.contentWindow.document.body – его <body> и так далее.

Когда-то…

В старых браузерах использовались другие свойства, такие как iframe.contentDocument и даже iframe.document, но они давно не нужны.

Переход внутрь ифрейма

В примере ниже JavaScript получает документ внутри ифрейма и модифицирует его:

<iframe src="javascript:'тест'" style="height:60px"></iframe>

<script>
  var iframe = document.getElementsByTagName('iframe')[0];

  var iframeDoc = iframe.contentWindow.document;

  if (iframeDoc.readyState == 'complete') {
    iframeDoc.body.style.backgroundColor = 'green';
  }
  iframe.onload = function() {
    var iframeDoc2 = iframe.contentWindow.document;
    iframeDoc2.body.style.backgroundColor = 'orange';
  }
</script>
src=„javascript:«текст»“

Атрибут src может использовать протокол javascript, как указано выше: src="javascript:код". При этом код выполняется и его результат будет содержимым ифрейма. Этот способ описан в стандарте и поддерживается всеми браузерами.

Атрибут src является обязательным, и его отсутствие может привести к проблемам, вплоть до игнорирования ифрейма браузером. Чтобы ничего не загружать в ифрейм, можно указать пустую строку: src="javascript:''" или специальную страницу: src="about:blank".

В некоторых браузерах (Chrome) пример выше покажет iframe зелёным. А в некоторых (Firefox) – оранжевым.

Дело в том, что, когда iframe только создан, документ в нём обычно ещё не загружен.

При обычных значениях iframe src="...", которые указывают на HTML-страницу (даже если она уже в кеше), это всегда так. Документ, который в iframe на момент срабатывания скрипта iframeDoc – временный, он будет заменён на новый очень скоро. И работать надо уже с новым документом iframeDoc2 – например, по событию iframe.onload.

В случае с javascript-протоколом, по идее, ифрейм уже загружен, и тогда onload у него уже не будет. Но здесь мнения браузеров расходятся, некоторые (Firefox) всё равно «подгрузят» документ позже. Поэтому факт «готовности» документа в скрипте проверяется через iframeDoc.readyState.

Ещё раз заметим, что при обычных URL в качестве src нужно работать не с начальным документом, а с тем, который появится позже.

Кросс-доменность: ограничение доступа к окну

Элемент <iframe> является «двуличным». С одной стороны, это обычный узел DOM, с другой – внутри находится окно, которое может иметь совершенно другой URL, содержать независимый документ из другого источника.

Внешний документ имеет полный доступ к <iframe> как к DOM-узлу. А вот к окну – если они с одного источника.

Это приводит к забавным последствиям. Например, чтобы узнать об окончании загрузки <iframe>, мы можем повесить обработчик iframe.onload. По сути, это то же самое что iframe.contentWindow.onload, но его мы можем поставить лишь в случае, если окно с того же источника.

<iframe src="https://example.com" style="height:100px"></iframe>

<script>
  var iframe = document.getElementsByTagName('iframe')[0];

  // сработает
  iframe.onload = function() {
    alert( "iframe onload" );
  };

  // не сработает
  iframe.contentWindow.onload = function() {
    alert( "contentWindow onload" );
  };
</script>

Если бы в примере выше <iframe src> был с текущего сайта, то оба обработчика сработали бы.

Иерархия window.frames

Альтернативный способ доступа к окну ифрейма – это получить его из коллекции window.frames.

Есть два способа доступа:

  1. window.frames[0] – доступ по номеру.
  2. window.frames.iframeName – доступ по name ифрейма.

Обратим внимание: в коллекции хранится именно окно (contentWindow), а не DOM-элемент.

Демонстрация всех способов доступа к окну:

<iframe src="javascript:''" style="height:80px" name="i"></iframe>

<script>
  var iframeTag = document.body.children[0];

  var iframeWindow = iframeTag.contentWindow; // окно из тега

  alert( frames[0] === iframeWindow ); // true, окно из коллекции frames
  alert( frames.i == iframeWindow ); // true, окно из frames по имени
</script>

Внутри ифрейма могут быть свои вложенные ифреймы. Всё это вместе образует иерархию.

Ссылки для навигации по ней:

  • window.frames – коллекция «детей» (вложенных ифреймов)

  • window.parent – содержит ссылку на родительское окно, позволяет обратиться к нему из ифрейма.

    Всегда верно:

    // (из окна со фреймом)
    window.frames[0].parent === window; // true
  • window.top – содержит ссылку на самое верхнее окно (вершину иерархии).

    Всегда верно (в предположении, что вложенные фреймы существуют):

    window.frames[0].frames[0].frames[0].top === window

Свойство top позволяет легко проверить, во фрейме ли находится текущий документ:

if (window == top) {
  alert( 'Этот скрипт является окном верхнего уровня в браузере' );
} else {
  alert( 'Этот скрипт исполняется во фрейме!' );
}

Песочница sandbox

Атрибут sandbox позволяет построить «песочницу» вокруг ифрейма, запретив ему выполнять ряд действий.

Наличие атрибута sandbox:

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

Пример ниже загружает в <iframe sandbox> документ с JavaScript и формой. Ни то ни другое не сработает:

Результат
index.html
sandboxed.html
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>

  <iframe sandbox src="sandboxed.html"></iframe>

</body>

</html>
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>

  <script>
    alert(1);
  </script>

  <form action="http://google.ru">
    <input type="text">
    <input type="submit" value="Отправить форму на http://google.ru">
  </form>

</body>

</html>

Если у атрибута sandbox нет значения, то браузер применяет максимум ограничений.

Атрибут sandbox может содержать через пробел список ограничений, которые не нужны:

allow-same-origin
Браузер будет считать документ в ифрейме пришедшим с другого домена и накладывать соответствующие ограничения на работу с ним. Если ифрейм и так с другого домена, то ничего не меняется.
allow-top-navigation
Разрешает ифрейму менять parent.location.
allow-forms
Разрешает отправлять формы из iframe.
allow-scripts
Разрешает выполнение скриптов из ифрейма. Но скриптам, всё же, будет запрещено открывать попапы.
На заметку:

Цель атрибута sandbox – наложить дополнительные ограничения. Он не может снять уже существующие, в частности, убрать ограничения безопасности, если ифрейм с другого источника.

Карта учебника