23 KiB
IFRAME для AJAX и COMET
Эта глава посвящена IFRAME
-- самому древнему и кросс-браузерному способу AJAX-запросов.
Сейчас он используется, разве что, для поддержки кросс-доменных запросов в IE7- и, что чуть более актуально, для реализации COMET в IE9-.
Для общения с сервером создается невидимый IFRAME
. В него отправляются данные, и в него же сервер пишет ответ.
[cut]
Введение
Сначала -- немного вспомогательных функций и особенности работы с IFRAME
.
Двуличность IFRAME: окно+документ
Что такое IFRAME? На этот вопрос у браузера два ответа
- IFRAME -- это HTML-тег:
<iframe>
со стандартным набором свойств.- Тег можно создавать в JavaScript
- У тега есть стили, можно менять.
- К тегу можно обратиться через `document.getElementById` и другие методы.
- 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, """);
// в старых IE нельзя указать name после создания input
// поэтому используем innerHTML вместо DOM-методов
html.push("<input type='hidden' name=\"" + key + "\" value=\"" + value + "\">");
}
phonyForm.innerHTML = html.join('');
phonyForm.submit();
}
Эта функция формирует форму динамически, но, конечно, это лишь один из возможных сценариев использования.
В IFRAME
можно отправлять и существующую форму, включающую файловые и другие поля.
Запросы GET и POST
Общий алгоритм обращения к серверу через ифрейм:
- Создаём `iframe` со случайным именем `iframeName`.
- Создаём в основном окне объект `CallbackRegistry`, в котором в `CallbackRegistry[iframeName]` сохраняем функцию, которая будет обрабатывать результат.
- Отправляем GET или POST-запрос в него.
- Сервер отвечает как-то так:
<!--+ no-beautify --> <script> parent.CallbackRegistry[window.name]({данные}); </script>
...То есть, вызывает из основного окна функцию обработки (
window.name
в ифрейме -- его имя). - Дополнительно нужен обработчик `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 тут же выполняется браузером, передавая информацию на основную страницу.
Таким образом, для передачи данных используется "бесконечный" ифрейм, через который сервер присылает все новые данные.
Схема работы:
- Создаётся `