AJAX-транспорт IFrame

Этот транспорт - пожалуй, самый универсальный и мощный, но и тонкостей в нем - больше всех

Для общения с сервером создается невидимый IFrame. Простая смена URL этого iframe - запрос к серверу за данными. Кроме того, в iframe можно отправлять post-запросы
поставив его имя в атрибут form.target.

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

Особенности

  • Возможна отправка файлов пользователя с диска: POST формы в iframe.

Проблемы

  • Звук "клика" при переходе запросе в iframe
  • Изменения в history браузера, влияющие на историю посещенных страниц и/или кнопки back-forward. Переходы по служебным URL не должны отражаться на history.
  • Полоса загрузки или курсор-часики при запросе в iframe. Запросы должны быть по возможности прозрачны, невидимы для посетителя.

А теперь - к реализации и ее особенностям, включая преодоление описанных проблем!

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

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

  1. HTML-тэг: <iframe> со стандартным набором свойств
    • Тэг можно создавать в javascript
    • У тега есть стили, можно менять style.height, style.width и т.п.
    • К тегу можно обратиться через document.getElementsByName(name)[0] или document.getElementById(id)
  2. Окно браузера, window
    • Такое же по функционалу окно браузера, как и основное, с адресом и т.п.
    • Основное окно и ифрейм могут общаться через javascript, если находятся на одном домене, или на разных поддоменах одного домена 2 уровня (same origin policy).
    • Можно получить через window.frames['имя фрейма']

Когда мы говорим о переводе iframe на новый URL - подразумеваем "окно". Когда собираемся создавать его и запихивать в DOM - конечно, "тег".

В теге iframe хранится ссылка на окно. В зависимости от браузера, это либо iframe.contentDocument, либо iframe.contentWindow.document, либо iframe.document.

// получить окно по тегу
function getIframeDocument(iframeNode) {
  if (iframeNode.contentDocument) return iframeNode.contentDocument
  if (iframeNode.contentWindow) return iframeNode.contentWindow.document
  return iframeNode.document
}

Из страницы внутри окна iframe можно пройти к родительскому окну через window.parent, и, если разрешает same origin policy, даже вызвать функцию/получить тег iframe.

Отправка запросов на сервер через iframe

GET

GET-запрос - всего лишь перевод iframe на новый URL. Его лучше всего осуществлять вызовом iframeDocument.location.src.replace(newURL).
Такой синтаксис в отличие от сабмита GET-формы в iframe или iframeDocument.location.src=..., оставляет history чистой в ряде браузеров. Так что переходы между адресами внутри iframe не отразятся на
кнопках back-forward и списке посещенных страниц..

function setIframeSrc(iframeNode, src) {
  getIframeDocument(iframeNode).location.replace(src)
}

POST

Для поста - достаточно задать форме form атрибут form.target='имя ифрейма' и вызвать form.submit(). Таким способом можно отправлять на сервер файлы, и вообще,
все что может содержать форма HTML.

// функция постит объект-хэш content в виде формы с нужным action, target
// напр. postToIframe({a:5,b:6}, '/count.php', 'frame1')

function postToIframe(content, action, target){
	if(typeof phonyForm == 'undefined'){
		// временную форму создаем, если нет
		phonyForm = document.createElement("form")
		phonyForm.style.display = "none"
		phonyForm.enctype = "application/x-www-form-urlencoded"
		phonyForm.method = "POST"        
		document.body.appendChild(phonyForm)
	}

	phonyForm.action = action
	phonyForm.target = target
	phonyForm.setAttribute("target", target);

	// убить все содержание из временной формы
	while(phonyForm.firstChild){
		phonyForm.removeChild(phonyForm.firstChild);
	}

	// заполнить форму данными из объекта
	for(var x in content){
		var tn;
		if(browser.isIE){
			tn = document.createElement("<input type='hidden' name='"+x+"' value='"+content[x]+"'>")
			phonyForm.appendChild(tn)
		}else{
			tn = document.createElement("input");
			phonyForm.appendChild(tn);
			tn.type = "hidden";
			tn.name = x;
			tn.value = content[x]
		}
	}
	phonyForm.submit();
}

Ответ сервера

Сервер получает запрос, и генерирует в ответ страницу, которая, как правило, передает результат в основное окно через специальную функцию-каллбэк.
В примерах такая функция появится чуть позже, при рассмотрении невидимых ифреймов.

iframe и history


Для браузера iframe - такое же окно, как и основное. Соответственно, переходы в нем на разные URL должны попадать в историю, браузить туда-сюда можно через back/forward.
Например, вот:

Туда (GET)
Сюда (GET)
Сюда (POST)

Клик на Туда соответствует переходу по адресу there.html, а клик на Сюда - по адресу here.html (он же - начальный адрес).

Вы можете скопировать ссылку на этот пример и позаходить на нее в разных браузерах. Клики на кнопки туда-сюда никак не отражаются на истории, хотя
меняют страницу внутри фрейма. Пока что остается звук клика в IE. Запросы к серверу должны быть, вроде как, незаметны, так что придется от него избавляться.

Для начала проверим - загрязняется ли history. Для этого в новой вкладке(или окне) откроем урл примера. Затем, нажимая на кнопки - смотрим, не заработала ли
кнопка back. Если заработала - в history появился новый, лишний элемент. Лишний - потому, что автоматические, служебные переходы по специальным адресам
попадать в history не должны, они там лишние.

Кроме того, генерирует ли IE звук клика при переходе по служебному адресу? Как правило, при стандартной звуковой схеме Windows такой звук есть.

Результаты тестов зависят от браузера, ОС и т.п. Обычно получиться так, что при POST history загрязняется, а при GET - нет.

Динамически создаем iframe

Создать ифрейм - так же просто, как и любой другой элемент. Пожалуй, единственная подстава - в IE свойство name должно обязательно задаваться при создании элемента.
Т.е, нельзя сначала сделать iframe, а потом присвоить ему name (то же самое и для input, и т.п.) - будут проблемы. Поэтому приходится делать отдельную проверку на isIE.

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

Если не задан параметр debug, то ифрейм после создания делается невидимым.

Динамическое создание ифрейма преследует две цели:

  1. Делаем HTML чище
  2. Динамический ифрейм - еще одна мера против загрязнения history
// браузер хранится в объекте browser
function createIFrame(fname, src, debug){
	var ifrstr = browser.isIE ? '<iframe name="'+fname+'" src="'+src+'">' : 'iframe'
	var cframe = document.createElement(ifrstr)

	with(cframe){ 
		name = fname // это не для IE
		setAttribute("name", fname) // и это тоже, но вреда не будет
		id = fname // а это везде ок
	}
	
	// можно добавлять сразу к document.body
	document.getElementById('iframe_container').appendChild(cframe);

	if (!debug) {
		hideIframe(cframe)
	}			
	
	if(!browser.isIE){
		setIframeSrc(cframe, src);
	}
	
	return cframe
}

// прячем фрейм
function hideIframe(iframeNode) {
	with(iframeNode.style) {
		if(!browser.isSafari){			
			position = "absolute";
		}
		left = top = "0px";
		height = width = "1px";
		visibility = "hidden";       
	}
}

Вы можете самостоятельно протестировать влияние динамической генерации на звук клика в IE и кнопки back-forward.

Решаем сразу все проблемы в IE и получаем идеальный транспорт IE-only.

Следующий пример работает только в IE. Он создает невидимый iframe, при запросе через который нет ни клика ни лишней хистори, ни индикатора загрузки.

Если Вы находитесь в IE, то можете заметить, что переходы по этому ифрейму абсолютно незаметны для пользователя. Идеал, да и только . Он создается при помощи
слабо документированной, но безопасной возможности ActiveX. Никаких специальных опций браузера для нее включать не надо.

function handleMessage(txt) { // функция-обработчик сообщения с сервера, живет в основном окне
	alert(">> "+txt) // для примера
}

// создать IE-only транспорт
function createIEFrame(fname, src) {
	// создаем объект htmlfile, примерно аналогичный по функционалу window.document
	var rcvNode = new ActiveXObject("htmlfile");
	// заполним исходным HTML
	rcvNode.open();
	rcvNode.write("<html><head><title>ActiveX</title></head><body></body></html>");
	rcvNode.close();
	
	// связь htmlfile с родительским окном (см объяснения ниже)
	rcvNode.parentWindow.deliver = handleMessage    
	
	// добавим внутрь div с нужным ифреймом
	var ifrDiv = rcvNode.createElement("div");
	rcvNode.appendChild(ifrDiv);
	ifrDiv.innerHTML = "<iframe name='"+fname+"' src='"+src+"'></iframe>"
	
	// глобальные переменные, для примера
	// главное - чтобы объект ActiveX и фрейм были доступны из исходного окна
	IEFrameNode = ifrDiv.firstChild
	IEFrameDocument = rcvNode
}

Как видно из примера, для изоляции ифрейма создается промежуточный документ ActiveX. Поэтому операции над ифреймом и не видны из основного окна.Здесь получается, что объектов window не два, как обычно (основное окно + ифрейм), а три:

  1. Основное окно window
  2. Окно документа htmlfile - доступно как IEFrameDocument.parentWindow
  3. Окно ифрейма - доступно как IEFrameNode.contentWindow, или с использованием getIframeDocument

Глобальные переменные IEFrameNode, IEFrameDocument дают нам прямой доступ из основного окна в окна 2 и 3. Так что можно легко отправить запрос на сервер вызовом

setIframeSrc(IEFrameNode, '/server_push/parentHere.html')...

.. Но что дальше? Документ с сервера, наверное, захочет обратиться к основному окну. Например, вызвать его функцию handleMessage() с некоторым сообщением.

И здесь есть некоторые сложности. Чтобы вызвать функцию основного окна, javascript-код изнутри ифрейма должен пробиться через промежуточный htmlfile.
Но htmlfile - это отдельный HTML-документ. Политика безопасности same origin policy запрещает взаимный javascript-доступ между документами
с сайта и новосозданным htmlfile.

Чтобы такой доступ получить, нужно заранее, из основного окна, протянуть ссылку из окна htmlfile в основное окно:

// rcvNode: htmlfile, объект, сходный с document
// rcvNode.parentWindow - окно, соответствующее rcvNode
// делаем ссылку из этого окна на функцию из основного окна
rcvNode.parentWindow.deliver = handleMessage

Если ссылка между окнами уже существует, то same origin policy не проверяется, так что документ с сервера может быть, например, таким:

<script>window.parent.deliver("here")</script>

Обмен данными проиллюстрирован на рисунке:

POST в ActiveX -> iframe

Если Вы решите использовать этот подход - позвольте мне сэкономить возможные часы копания в отладчике. До этого обсуждался метод GET.
В нем достаточно ссылки на iframe.

С другой стороны, в методе POST нужно присвоить form.target не сам иферейм, а имя ифрейма, причем этот ифрейм должен быть виден из текущего окна.

Легко проверить, что простой пост в фрейм "frame3" (в примере) не даст результата, т.к этот фрейм не виден.

// не пашет, откроет /server_push/parentHere.html в новом окне, т.к не увидит frame3
postToIframe({prop:'val'}, '/server_push/parentHere.html', 'frame3')

Для того, чтобы форма увидела ифрейм, ее нужно создать в том же окне. Это можно сделать массой способов. Например, добавить нужные скрипты в htmlfile:

...
rcvNode.open();
rcvNode.write("<html><head><title>ActiveX</title>")
rcvNode.write("<script src='/http/javascript.ru/js/browser.js'></sc"+"ript>")
rcvNode.write("<script src='/http/javascript.ru/server_push/iframe.js'></scr"+"ipt>")
rcvNode.write("</head><body></body></html>")
rcvNode.close();
...

И вызвать отправку формы из нужного окна:

IEFrameDocument.parentWindow.postToIframe({prop:'val'}, '/server_push/parentHere.html', 'frame3')

Все основное, надеюсь, в тексте есть. Если есть более глубокий интерес - приятного копания в отладчике .

Описанный способ предлагает идеальную реализацию iframe-транспорта, которая, к сожалению, работает только в IE. А для остальных браузеров можно использовать либо обычный iframe-транспорт, либо другие транспорты.

Базовый пример использования iframe для COMET c использованием GET вы можете скачать.