en.javascript.info/4-ajax/12-ajax-iframe/article.md
Ilya Kantor 6bf5977407 fixes
2015-05-14 10:40:28 +03:00

23 KiB
Raw Blame History

IFRAME для AJAX и COMET

Эта глава посвящена IFRAME -- самому древнему и кросс-браузерному способу AJAX-запросов.

Сейчас он используется, разве что, для поддержки кросс-доменных запросов в IE7- и, что чуть более актуально, для реализации COMET в IE9-.

Для общения с сервером создается невидимый IFRAME. В него отправляются данные, и в него же сервер пишет ответ.

[cut]

Введение

Сначала -- немного вспомогательных функций и особенности работы с IFRAME.

Двуличность IFRAME: окно+документ

Что такое IFRAME? На этот вопрос у браузера два ответа

  1. IFRAME -- это HTML-тег: <iframe> со стандартным набором свойств.
    • Тег можно создавать в JavaScript
    • У тега есть стили, можно менять.
    • К тегу можно обратиться через `document.getElementById` и другие методы.
  2. IFRAME -- это окно браузера, вложенное в основное
    • IFRAME -- такое же по функционалу окно браузера, как и основное, с адресом и т.п.
    • Если документ в `IFRAME` и внешнее окно находятся на разных доменах, то прямой вызов методов друг друга невозможен.
    • Ссылку на это окно можно получить через `window.frames['имя фрейма']`.

Для достижения цели мы будем работать как с тегом, так и с окном. Они, конечно же, взаимосвязаны.

В теге <iframe> свойство contentWindow хранит ссылку на окно.

Окна также содержатся в коллекции window.frames.

Например:

// Окно из ифрейма
var iframeWin = iframe.contentWindow;

// Можно получить и через frames, если мы знаем имя ифрейма (и оно у него есть)
var iframeWin = window.frames[iframe.name];
iframeWin.parent == window; // parent из iframe указывает на родительское окно

// Документ не будет доступен, если iframe с другого домена
var iframeDoc = iframe.contentWindow.document;

Больше информации об ифреймах вы можете получить в главе .

IFRAME и история посещений

IFRAME -- полноценное окно, поэтому навигация в нём попадает в историю посещений.

Это означает, что при нажатии кнопки "Назад" браузер вернёт посетителя назад не в основном окне, а в ифрейме. В лучшем случае -- браузер возьмёт предыдущее состояние ифрейма из кэша и посетитель просто подумает, что кнопка не сработала. В худшем -- в ифрейм будет сделан предыдущий запрос, а это уже точно ни к чему.

Наши запросы в ифрейм -- служебные и для истории не предназначены. К счастью, есть ряд техник, которые позволяют обойти проблему.

  • Ифрейм нужно создавать динамически, через JavaScript.
  • Когда ифрейм уже создан, то единственный способ поменять его `src` без попадания запроса в историю посещений:
    // newSrc - новый адрес
    iframeDoc.location.replace(newSrc);
    

    Вы можете возразить: "но ведь iframeDoc не всегда доступен! iframe может быть с другого домена -- как быть тогда?". Ответ: вместо смены src этого ифрейма -- создать новый, с новым src.

  • POST-запросы в `iframe` всегда попадают в историю посещений.
  • ... Но если `iframe` удалить, то лишняя история тоже исчезнет :). Сделать это можно по окончании запроса.

Таким образом, общий принцип использования IFRAME: динамически создать, сделать запрос, удалить.

Бывает так, что удалить по каким-то причинам нельзя, тогда возможны проблемы с историей, описанные выше.

Функция createIframe

Приведенная ниже функция createIframe(name, src, debug) кросс-браузерно создаёт ифрейм с данным именем и src.

Аргументы:

`name`
Имя и `id` ифрейма
`src`
Исходный адрес ифрейма. Необязательный параметр.
`debug`
Если параметр задан, то ифрейм после создания не прячется.
function createIframe(name, src, debug) {
  src = src || 'javascript:false'; // пустой src

  var tmpElem = document.createElement('div');

  // в старых IE нельзя присвоить name после создания iframe
  // поэтому создаём через innerHTML
  tmpElem.innerHTML = '<iframe name="' + name + '" id="' + name + '" src="' + src + '">';
  var iframe = tmpElem.firstChild;

  if (!debug) {
    iframe.style.display = 'none';
  }

  document.body.appendChild(iframe);

  return iframe;
}

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

Кстати, при вставке, если не указан src, тут же произойдёт событие iframe.onload. Пока обработчиков нет, поэтому оно будет проигнорировано.

Функция postToIframe

Функция postToIframe(url, data, target) отправляет POST-запрос в ифрейм с именем target, на адрес url с данными data.

Аргументы:

`url`
URL, на который отправлять запрос.
`data`
Объект содержит пары `ключ:значение` для полей формы. Значение будет приведено к строке.
`target`
Имя ифрейма, в который отправлять данные.
// Например: postToIframe('/vote', {mark:5}, 'frame1')

function postToIframe(url, data, target) {
  var phonyForm = document.getElementById('phonyForm');
  if (!phonyForm) {
    // временную форму создаем, если нет
    phonyForm = document.createElement("form");
    phonyForm.id = 'phonyForm';
    phonyForm.style.display = "none";
    phonyForm.method = "POST";
    document.body.appendChild(phonyForm);
  }

  phonyForm.action = url;
  phonyForm.target = target;

  // заполнить форму данными из объекта
  var html = [];
  for (var key in data) {
    var value = String(data[key]).replace(/"/g, "&quot;");
    // в старых IE нельзя указать name после создания input
    // поэтому используем innerHTML вместо DOM-методов
    html.push("<input type='hidden' name=\"" + key + "\" value=\"" + value + "\">");
  }
  phonyForm.innerHTML = html.join('');

  phonyForm.submit();
}

Эта функция формирует форму динамически, но, конечно, это лишь один из возможных сценариев использования.

В IFRAME можно отправлять и существующую форму, включающую файловые и другие поля.

Запросы GET и POST

Общий алгоритм обращения к серверу через ифрейм:

  1. Создаём `iframe` со случайным именем `iframeName`.
  2. Создаём в основном окне объект `CallbackRegistry`, в котором в `CallbackRegistry[iframeName]` сохраняем функцию, которая будет обрабатывать результат.
  3. Отправляем GET или POST-запрос в него.
  4. Сервер отвечает как-то так:
    <!--+ no-beautify -->
    <script>
      parent.CallbackRegistry[window.name]({данные});
    </script>
    

    ...То есть, вызывает из основного окна функцию обработки (window.name в ифрейме -- его имя).

  5. Дополнительно нужен обработчик `iframe.onload` -- он сработает и проверит, выполнилась ли функция `CallbackRegistry[window.name]`. Если нет, значит какая-то ошибка. Сервер при нормальном потоке выполнения всегда отвечает её вызовом.

Подробнее можно понять процесс, взглянув на код.

Мы будем использовать в нём две функции -- одну для GET, другую -- для POST:

  • `iframeGet(url, onSuccess, onError)` -- для GET-запросов на `url`. При успешном запросе вызывается `onSuccess(result)`, при неуспешном: `onError()`.
  • `iframePost(url, data, onSuccess, onError)` -- для POST-запросов на `url`. Значением `data` должен быть объект `ключ:значение` для пересылаемых данных, он конвертируется в поля формы.

Пример в действии, возвращающий дату сервера при GET и разницу между датами клиента и сервера при POST:

[codetabs src="date"]

Прямой вызов функции внешнего окна из ифрейма отлично работает, потому что они с одного домена. Если с разных, то нужны дополнительные действия, например:

  • В IE8+ есть интерфейс [postMessage](https://developer.mozilla.org/en-US/docs/DOM/window.postMessage) для общения между окнами с разных доменов.
  • В любых, даже самых старых IE, можно обмениваться данными через `window.name`. Эта переменная хранит "имя" окна или фрейма, которое не меняется при перезагрузке страницы.

    Поэтому если мы сделали POST в <iframe> на другой домен и он поставил window.name = "Вася", а затем сделал редирект на основной домен, то эти данные станут доступны внешней странице.

  • Также в совсем старых IE можно обмениваться данными через хеш, то есть фрагмент URL после `#`. Его изменение доступно между ифреймами с разных доменов и не приводит к перезагрузке страницы. Таким образом они могут передавать данные друг другу. Есть готовые библиотеки, которые реализуют этот подход, например [Porthole](http://ternarylabs.github.io/porthole/).

IFRAME для COMET

Бесконечный IFRAME -- самый старый способ организации COMET. Когда-то он был основой AJAX-приложений, а сейчас -- используется лишь в случаях, когда браузер не поддерживает современный стандарт WebSocket, то есть для IE9-.

Этот способ основан на том, что браузер читает страницу последовательно и обрабатывает все новые теги по мере того, как сервер их присылает.

Классическая реализация -- это когда клиент создает невидимый IFRAME, ведущий на служебный URL. Сервер, получив соединение на этот URL, не закрывает его, а время от времени присылает блоки сообщений <script>...javascript...</script>. Появившийся в IFRAME'е javascript тут же выполняется браузером, передавая информацию на основную страницу.

Таким образом, для передачи данных используется "бесконечный" ифрейм, через который сервер присылает все новые данные.

Схема работы:

  1. Создаётся `