Здесь Вы найдете полное описание объекта XMLHTTPRequest, способы использования, форматы данных и разбор частых проблем.

Илья Кантор (c) 2007
Полезного чтения.

P.S Вопросы можно задавать на форуме http://forum.client-side.ru

Объект XMLHttpRequest

Объект XMLHttpRequest (или, сокращенно, XHR) дает возможность браузеру делать HTTP-запросы к серверу без перезагрузки страницы.

Несмотря на слово XML в названии, XMLHttpRequest может работать с данными в любом текстовом формате, и даже бинарными данными. Использовать его очень просто.

Кроссбраузерное создание объекта запроса

В зависимости от браузера, код для создания объекта может быть разный. Кроссбраузерная функция создания XMLHttpRequest:

function getXmlHttp(){
  var xmlhttp;
  try {
    xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
  } catch (e) {
    try {
      xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    } catch (E) {
      xmlhttp = false;
    }
  }
  if (!xmlhttp && typeof XMLHttpRequest!='undefined') {
    xmlhttp = new XMLHttpRequest();
  }
  return xmlhttp;
}
Функция тупо перебирает возможные внутренние реализации и возвращает начальный объект XMLHttpRequest. Существует и масса других рабочих кроссбраузерных функций, однако все они по сути делают то же самое.

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

Различают два использования XmlHttpRequest. Первое - самое простое, синхронное.

Синхронный XMLHttpRequest

В этом примере через XMLHTTPRequest, с сервера запрашивается страница http://example.org/, и текст ответа сервера показывается через alert().
var req = getXmlHttp()
req.open('GET', '/xhr/test.html'); 
req.send(null);
if(req.status == 200) {
  alert(req.responseText);
}

Здесь сначала создается запрос, задается открытие (open) синхронного соединение с адресом /xhr/test.html и запрос отсылается с null, т.е без данных: send(null).

При синхронном запросе браузер "подвисает" и ждет на строчке 3, пока сервер не ответит на запрос. Когда ответ получен - выполняется строка 4, код ответа сравнивается с 200 (ОК), и при помощи alert печатается текст ответа сервера. Все максимально просто.

Свойство responseText получит такой же текст страницы, как браузер, если бы Вы в перешли на /xhr/test.html. Для сервера GET-запрос через XmlHttpRequest ничем не отличается от обычного перехода на страницу.

Асинхронный XMLHttpRequest

Этот пример делает то же самое, но асинхронно, т.е браузер не ждет выполнения запроса для продолжения скрипта. Вместо этого к свойству onreadystatechange подвешивается функция, которую запрос вызовет сам, когда получит ответ с сервера.
var req = getXmlHttp()
req.open('GET', '/xhr/test.html', true); 
req.onreadystatechange = function() {
  if (req.readyState == 4) {
     if(req.status == 200) {
       alert(req.responseText);
	 }
  }
};
req.send(null); 

Асинхронность включается третьим параметром функции open. В отличие от синхронного запроса, функция send() не останавливает выполнение скрипта, а просто отправляет запрос.

Запрос req регулярно отчитывается о своем состоянии через вызов функции req.onreadystatechange. Состояние под номером 4 означает конец выполнения, поэтому функция-обработчик при каждом вызове проверяет - не настало ли это состояние.

Вообще, список состояний readyState такой:

Состояния 0-2 вообще не используются.

Вызов функции с состоянием Interactive в теории должен происходить каждый раз при получении очередной порции данных от сервера. Это могло бы быть удобным для обработки ответа по частям, но Internet Explorer не дает доступа к уже полученной части ответа.
Firefox дает такой доступ, но для обработки запроса по частям состояние Interactive все равно неудобно из-за сложностей обнаружения ошибок соединения. Поэтому Interactive тоже не используется.

На практике используется только последнее, Complete.

Если хотите углубиться в тонкости багов браузеров c readyState, отличными от 4, то многие из них рассмотрены в статье на Quirksmode (англ.).

Не используйте синхронные запросы

Синхронные запросы применяются только в крайнем случае, когда кровь из носу необходимо дождаться ответа сервера до продолжения скрипта. В 999 случаях из 1000 можно использовать асинхронные запросы. При этом общий алгоритм такой:

  1. Делаем асинхронный запрос
  2. Рисуем анимированную картинку или просто запись типа "Loading..."
  3. В onreadystatechange при достижении состояния 4 убираем Loading и, в зависимости от status вызываем обработку ответа или ошибки.

Кроме того, иногда полезно ставить ограничение на время запроса. Например, хочется генерировать ошибку, если запрос висит более 10 секунд.

Для этого сразу после send() через setTimeout ставится вызов обработчика ошибки, который очищается при получении ответа и обрывает запрос с генерацией ошибки, если истекли 10 секунд.

Таймаут на синхронный запрос ставить нельзя, браузер может висеть долго-долго.. А вот на асинхронный - пожалуйста.

Этот пример демонстрирует такой таймаут.

var xhr = getXmlHttp()
xhr.open("POST", "/someurl", true);

xhr.onreadystatechange=function(){
  if (xhr.readyState != 4) return
  
  clearTimeout(xhrTimeout) // очистить таймаут при наступлении readyState 4

  if (xhr.status == 200) {
      // Все ок
      ...
      alert(xhr.responseText);
      ...
  } else {
      handleError(xhr.statusText) // вызвать обработчик ошибки с текстом ответа
  }
}

xhr.send("a=5&b=4");
// Таймаут 10 секунд
var xhrTimeout = setTimeout( function(){ xhr.abort(); handleError("Timeout") }, 10000);

function handleError(message) {
  // обработчик ошибки
  ...
  alert("Ошибка: "+message)
  ...
}

Методы объекта XMLHttpRequest

open()

Варианты вызова:

Первый параметр method - HTTP-метод. Как правило, используется GET либо POST, хотя доступны и более экзотические, вроде TRACE/DELETE/PUT и т.п.

URL - адрес запроса. Можно использовать не только HTTP/HTTPS, но и другие протоколы, например FTP и FILE://. При этом есть ограничения безопасности, так называемая "same origin policy": запрос со страницы можно отправлять только на тот домен и порт, с которого она пришла.

Ниже это ограничение и способы обхода будут рассмотрены подробнее.

async = true задает асинхронные запросы, эта тема была поднята выше.

userName, password - данные для HTTP-авторизации.

send()

Отсылает запрос. Аргумент - тело запроса. Например, GET-запроса тела нет, поэтому используется send(null), а для POST-запросов тело содержит параметры запроса.

abort()

Вызов этого метода req.abort() обрывает текущий запрос.

Здесь есть одно НО для браузера Internet Explorer. Успешный вызов abort() на самом деле может не обрывать соединение, а оставлять его в подвешенном состоянии на некоторый таймаут (20-30 секунд). Отловить такие повисшие соединения можно через прокси для отладки, например, Fiddler.

У браузера есть лимит: не более 2 одновременных соединений с одним доменом-портом. Т.е, если два соединения уже висят (и отвиснут по таймауту), то третье открыто не будет, пока одно из них не умрет. Надеюсь, Вы с такой проблемой не столкнетесь. Ее можно обойти использованием кросс-доменных XmlHttpRequest.

setRequestHeader(name, value)

Устанавливает заголовок name запроса со значением value. Если заголовок с таким name уже есть - он заменяется. Например,
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')

getAllResponseHeaders()

Возвращает строку со всеми HTTP-заголовками ответа сервера.

getResponseHeader(headerName)

Возвращает значение заголовка ответа сервера с именем headerName.

Свойства объекта XMLHttpRequest

onreadystatechange

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

readyState

Номер состояния запроса от 0 до 4. Используйте только 4 ("completed").

responseText

Текст ответа сервера. Полный текст есть только при readyState=4, ряд браузеров дают доступ к полученной части ответа сервера при readyState=3.

responseXML

Ответ сервера в виде XML, при readyState=4.

Это свойство хранит объект типа XML document, с которым можно обращаться так же, как с обычным document. Например,

var authorElem = req.responseXML.getElementById('author')

Чтобы браузер распарсил ответ сервера в свойство responseXML, в ответе должен быть заголовок Content-Type: text/xml.
Иначе свойство responseXML будет равно null.

status

Для HTTP-запросов - статусный код ответа сервера: 200 - OK, 404 - Not Found, и т.п. Браузер Internet Explorer может также присвоить status код ошибки WinInet, например 12029 для ошибки "cannot connect".

Запросы по протоколам FTP, FILE:// не возвращают статуса, поэтому нормальным для них является status=0.

statusText

Текстовая расшифровка status, например "Not Found" или "OK".

GET и POST-запросы. Кодировка.

При отправке обычной формы, браузер сам кодирует значения полей и составляет тело GET/POST-запроса. При работе через XmlHttpRequest, это нужно делать самим, в коде. Большинство проблем и вопросов здесь связано с непониманием, где и какое кодирование нужно осуществлять.

Есть два вида кодирования запроса. Первый - это метод GET и POST-формы с кодировкой urlencoded, она же - кодировка по умолчанию. В браузере они определяются как:

<form method="get"> // метод GET с кодировкой по умолчанию
<form method="post" enctype="application/x-www-form-urlencoded"> // enctype явно задает кодировку
<form method="post"> // метод POST с кодировкой по умолчанию (такой же, как выше)

При таком способе кодировки (urlencoded) название и значение каждой переменной кодируется функцией encodeURIComponent. Формируя XmlHttpRequest, мы должны делать то же самое "руками". Конечно, только с теми переменными, в которых могут быть спецсимволы или не английские буквы, т.е которые и будут как раз закодированы.

// Пример с GET
... 
var params = 'q=' + encodeURIComponent(input) + '&target=' + encodeURIComponent(target)
req.open("GET", '/script.html?'+params, true)
...
req.send(null)

В методе POST параметры передаются не в URL, а в теле, посылаемом через send(). Поэтому params нужно указывать не в адресе, а при вызове send()

Кроме того, при POST обязателен заголовок Content-Type, содержащий кодировку. Это указание для сервера - как обрабатывать (раскодировать) пришедший запрос.

// Пример с POST
... 
var params = 'q=' + encodeURIComponent(input) + '&target=' + encodeURIComponent(target)
req.open("POST", '/script.html', true)
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
...
req.send(params)

Заголовки Content-Length, Connection в POST-запросах, хотя их и содержат некоторые "руководства", обычно не нужны. Используйте их, только если Вы действительно знаете, что делаете.

Запросы multipart/form-data

Второй способ кодирования - это отсутствие кодирования. Например, это нужно для пересылки файлов. Он указывается в форме (только для POST) так:

<form method="post" enctype="multipart/form-data">

В этом случае ничего не кодируется. А сервер, со своей стороны, посмотрев на Content-Type(=multipart/form-data), поймет, что пришло.

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

Кодировка

Если Вы используете только UTF-8 - пропустите эту секцию.

Все идущие на сервер параметры GET/POST, кроме случая multipart/form-data, кодируются в UTF-8. Не в кодировке страницы, а именно в UTF-8. Поэтому, например, в PHP их нужно при необходимости перекодировать функцией iconv.

С другой стороны, когда ответ с сервера браузер воспринимает в той кодировке, которая указана в заголовке ответа Content-Type. Т.е, опять же, в PHP, чтобы браузер воспринял ответ в windows-1251 и нормально отобразил данные на странице в windows-1251, нужно послать заголовок в php-коде самим, типа:

// в php
header('Content-Type: text/plain; charset=windows-1251');
Или же, это должен сделать сервер. Например, в apache автоматически добавляется кодировка опцией:
# в конфиге апача
AddDefaultCharset windows-1251

Частые проблемы

Кеширование

Многие браузеры поддерживают кеширование ответов на XmlHttpRequest запросы. При этом реализации кеширования немного разные.

Например, при повторном XmlHttpRequest на тот же URL, Firefox посылает запрос с заголовком "If-Modified-Since" со значением, указанным в заголовке "Last-Modified" предыдущего ответа.

А Internet Explorer делает так, только когда кешированный ответ устарел, т.е после времени из заголовка "Expires" предыдущего ответа. Поэтому, кстати, многие думают, что Internet Explorer вообще не очищает кеш ответов.

Самое простое решение проблемы - просто убрать кеширование. Например, при помощи заголовков, или добавлением случайного параметра в URL типа:

req.open("GET", "/service.php?r="+Math.random(), true)

Есть, однако, ряд случаев, когда кеширование XMLHttpRequest браузером полезно, улучшает время ответа и экономит трафик.

Пример демонстрирует универсальный код работы с кешем для Internet Explorer и Firefox.

var req = getXmlHttp()
req.open("GET", uri, false); // синхронный запрос для примера
req.send(null);
if(!req.getResponseHeader("Date")) {
  var cached = req;
  req = getXmlHttp()
  var ifModifiedSince = cached.getResponseHeader("Last-Modified");
  ifModifiedSince = (ifModifiedSince) ? ifModifiedSince : new Date(0); // January 1, 1970
  req.open("GET", uri, false);
  req.setRequestHeader("If-Modified-Since", ifModifiedSince);
  req.send("");
  if(req.status == 304)  {
    req = cached;
  }
}

В Internet Explorer, если запрос возвращается из кеша без перепроверки, заголовок Date - пустая строка. Поэтому нужно сделать дополнительный запрос, который на самом деле никакой не дополнительный, т.к текущий возвращен из кеша.

Ссылку на кешированый запрос сохраняем, т.к если код ответа дополнительного запроса - "304 Not Modified", то его тело станет пустой строкой (""), и нужно будет вернуться к кешированному объекту. Более эффективным, впрочем, будет не создавать новый объект XmlHttpRequest, а сохранить данные из существующего и использовать заново его же.

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

Повторное использование объекта XmlHttpRequest

В Internet Explorer, если open() вызван после установки onreadystatechange, может быть проблема с повторным использованием этого XmlHttpRequest.

Чтобы использовать заново XmlHttpRequest, сначала вызывайте метод open(), а затем - присваивайте onreadystatechange. Это нужно потому, что IE неявно очищает объект XmlHttpRequest в методе open(), если его статус "completed".

Вызывать abort() для перенаправления запроса на другой URL не нужно, даже если текущий запрос еще не завершился.

Утечки памяти

В Internet Explorer объект XmlHttpRequest принадлежит миру DOM/COM, а Javascript-функция - миру Javascript. Вызов req.onreadystatechange = function() { ... } неявную круговую связь: req ссылается на функцию через onreadystatechange, а функция, через область видимости - видит (ссылается на) req.

Невозможность обнаружить и оборвать такую связь во многих (до IE 6,7 редакции июня 2007?) версиях Internet Explorer приводит к тому, что XmlHttpRequest вместе с ответом сервера, функция-обработчик, и всё замыкание прочно оседают в памяти до перезагрузки браузера.

Чтобы этого избежать, ряд фреймворков (YUI, dojo...) вообще не ставят onreadystatechange, а вместо этого через setTimeout проверяют его readyState каждые 10 миллисекунд. Это разрывает круговую связку req <-> onreadystatechange, и утечка памяти не грозит даже в самых глючных браузерах.

Ограничения безопасности. Кросс-доменный XMLHttpRequest

Для ограничения XmlHttpRequest используется философия "Same Origin Policy". Она очень проста - каждый сайт в своей песочнице. Запрос можно делать только на адреса с тем же протоколом, доменом, портом, что и текущая страница.

Т.е, со страницы на адресе http://site.com нельзя сделать XmlHttpRequest на адрес https://site.com, http://site.com:81 или http://othersite.com

Это создает проблему, если хочется взять контент с другого сайта. Как правило, в этом случае вместо XmlHttpRequest используются другие средства, например, загрузка через динамически создаваемый тег <script>. Но, конечно, XmlHttpRequest удобнее.

Проксирование

Самый простой способ обойти это ограничение - проксирование. Допустим, мы хотим сделать запрос с http://site.com на http://remote.com/get.html. Вместо указания remote.com в методе open(), там ставится URL вида http://site.com/proxy/remote.com/get.html. Запрос приходит на наш веб-сервер, проксирует его на сервер на site.com, который уже обрабатывает этот запрос, как нужно.

Если remote.com находится на другом сервере, то серверу site.com придется проксировать посетителю как запрос, так и ответ. При этом, разумеется, никак не будут задействованы куки remote.com, так что не получится отдельной авторизации, учета пользователей или чтото в этом роде с отдельными куками.

Например, при использовании web-сервера Apache, для проксирования нужны директивы ProxyPass, ProxyPassReverse. Кроме того, доступны еще модули, которые по необходимости правят урлы, разархивируют контент и т.п.

Использование наддомена

Часто кроссбраузерные запросы - это
  1. Способ обойти ограничения в 2 одновременных соединения к одному домену-порту.
  2. Способ использовать два разных сервера в общении с посетителем. Например, на chat.site.ru - чат-демон, на www.site.ru - веб-сервер.

Кросс-доменные запросы между наддоменами http://a.site.com, http://b.site.com (на общем site.com) допустимы при использовании свойства document.domain, которое надо установить в site.com

// на странице a.site.com
...
document.domain='site.com'
...
// все, теперь могу делать XmlHttpRequest на site.com
req.open("POST", 'http://site.com/giveme.php')

Любые запросы допустимы между сайтами, находящимися в доверенной (trusted) зоне Internet Explorer. Так что, внутренний корпоративный портал может быть у всех пользователей в этой зоне, и он сможет делать запросы к любым сайтам.

XhrIframeProxy

Еще один хитрый подход называется XHRIframeProxy, и позволяет делать XmlHttpRequest к любым доменам при помощи хитрого iframe-хака. Он основан на том, что фреймы с разных доменов могут читать и менять друг у друга anchor, т.е часть адреса после решетки '#'. За счет этого организуется специальный протокол, по которому "проксируется" XmlHttpRequest.

Этот метод, в принципе, вполне жизнеспособен, особенно для небольшого объема данных.

Поддержка в библиотеках и фреймворках

Практически каждая javascript-библиотека или javascript-фреймворк включает в том или ином виде поддержку XmlHttpRequest-запросов и других способов прозрачного общения с сервером. Берите фреймворк по другим параметрам, а какая-то поддержка так обязательно будет.

Javascript-библиотеки

Наиболее профессионально общение с сервером, на мой взгляд, сделано в dojo. Для удобства работы с асинхронными вызовами, в dojo и Mochikit используется специальный объект Deferred. Умеет посылать формы, отменять запросы, позволяет строить сложные цепочки асинхронных вызовов. В dojo для этого используется вызов dojo.xhrGet, который позволяет указывать обработчик, таймаут и формат запроса (например, JSON). Также умеет предотвращать кеширование (preventCache), передавать объекты/формы с файлами.

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

В Yahoo UI соединениями с сервером заведует Connection Manager. Главная фунция asyncRequest принимает в качестве одного из параметров (callback) объект, который позволяет подписываться на события, указывать timeout и посылать на сервер объект. Кроме того можно указывать временной промежуток для автоматических опросов. Например, опрашивать новости с сервера каждые 3 секунды. Метод setForm передает форму, умеет загружать файлы.

Во фреймворке prototype Ajax представлен рядом классов вида Ajax.*. В сочетании с другими методами библиотеки - предоставляет весь стандартный функционал. Кроме того - приятный бонус: Ajax.PeriodicalUpdater умеет легко обновлять HTML-элемент с сервера и гибко увеличивать промежуток между опросами при проблемах серверной части.

Есть еще библиотека JsHttpRequest, которая набрала популярность за счет русской документации и коммунити. Если Вы не знаете английского языка - вперед и с песнями. Весь базовый функционал у нее есть. Лично я не разу не пользовался, но говорят - работает.

Серверные библиотеки

Есть специальные серверные библиотеки, которые упрощают работу с XmlHttpRequest, организуя не только javascript-часть, но и серверную тоже. Они обычно умеют, например, отображать серверные функции на php в javascript-аналоги. При вызове такого javascript-аналога библиотека сама сделает запрос на сервер, обработает его на сервере, вызовет серверную функцию и вернет ее результат.

Для PHP одной из лучших библиотек является XAJAX, для Java - DWR, Для Ruby.. сами знаете.

...А если...

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

Если на странице не оказалось той информации, которую Вы искали - напишите о ней в форме предложений.
Спасибо!

Внесите, пожалуйста, свои пожелания в форму:
наверх