Источник: http://learn.javascript.ru/ajax-xmlhttprequest
Объект XMLHttpRequest
(или, как его кратко называют, «XHR») дает возможность браузеру делать HTTP-запросы к серверу без перезагрузки страницы.
Несмотря на слово XML в названии, XMLHttpRequest
может работать с данными в любом текстовом формате, и даже c бинарными данными.
Рассмотрим пример с кнопкой для голосования.
Внутри он состоит из трёх частей:
<button onclick="vote(this)">Голосовать</button>
Функция голосования:
function vote(outputElem) {
var xhr = new XMLHttpRequest(); // (1)
xhr.open('GET', '/files/tutorial/ajax/xhr/vote', true); // (2)
xhr.onreadystatechange = function() { // (3)
if (xhr.readyState != 4) return; // (3.1)
outputElem.innerHTML = xhr.responseText; // (3.2)
}
outputElem.innerHTML = '...';
xhr.send(null); // (4)
}
res.end('Ваш голос принят: ' + new Date());
Разберём функцию голосования по шагам:
XMLHttpRequest
. Этот объект предназначен для отправки запроса на сервер.xhr
конфигурируется: нам нужен GET-запрос на vote
. На этом этапе соединение с сервером ещё не открыто.xhr.onreadystatechange
задаёт, что делать при ответе сервера.xhr.readyState
. Нас интересует только 4 (запрос завершён).xhr.responseText
содержит текст ответа сервера.xhr.send(null)
.Далее мы посмотрим методы и события более подробно.
Эти три метода управляют основным потоком запроса:
open(method, URL, async, user, password)
Задаёт основные параметры запроса:
ftp://
и file://
. При этом есть ограничения безопасности, называемые «Same Origin Policy»: запрос со страницы можно отправлять только на тот же протокол://домен:порт, с которого она пришла.send()
.send(body)
Отправить запрос на сервер.
В body
находится тело запроса. Не у всякого запроса есть тело, например у GET-запросов тела нет, в таком случае передаётся null или пустая строка.
С другой стороны, в POST основные данные как раз передаются через body
.
abort()
Прерывает выполнение запроса.
Синхронный вызов XMLHttpRequest
происходит, если параметр async
равен false. В этом случае страница «подвисает»: скрипт ждёт ответа сервера, а затем продолжается — и ответ сервера уже можно использовать:
function voteSync(outputElem) {
var xhr = new XMLHttpRequest(); // (1)
xhr.open('GET', '/files/tutorial/ajax/xhr/vote', false);
outputElem.innerHTML = '...';
xhr.send(null); // (2)
outputElem.innerHTML = xhr.responseText; // (3)
}
При синхронном запросе скрипт останавливается, и страница «подвисает», пока сервер не ответит.
Если запрос — слишком долгий, то большинство браузеров предложат посетителю «убить» процесс с «зависшим» скриптом.
Многие продвинутые возможности XMLHttpRequest
, которые мы обсудим в следующих главах, не работают в синхронном режиме.
В частности, не работают кросс-доменные запросы и нельзя указать таймаут.
Всё это делает синхронный XMLHttpRequest
редким и нежелательным гостем в веб-приложениях. Далее мы будем использовать только асинхронные запросы.
Выше мы посмотрели код, иллюстрирующий простейшее использование onreadystatechange
и responseText
. Теперь — время подробнее ознакомиться с свойствами и событиями этого объекта.
Событие readystatechange
происходит несколько раз в процессе отсылки и получения ответа. При этом можно посмотреть «текущее состояние запроса» в свойстве xhr.readyState
, которое принимает значения от 0 до 4.
Состояния, по спецификации.
const unsigned short UNSENT = 0; // начальное состояние
const unsigned short OPENED = 1; // вызван open
const unsigned short HEADERS_RECEIVED = 2; // получены заголовки
const unsigned short LOADING = 3; // загружается тело
const unsigned short DONE = 4; // запрос завершён
Надёжно и кросс-браузерно работает только последнее состояние: 4 (запрос завершён).
Типичная проверка конца запроса:
xhr.onreadystatechange = function() {
if (xhr.readyState != 4) return; // запрос ещё не завершён
// .. обработать завершение запроса, проверить ошибки
}
Остальные состояния:
onreadystatechange
, как только получен достаточно большой пакет данных.Пример ниже демонстрирует переключение между состояниями. В нём сервер отвечает на запрос, пересылая по цифре в секунду:
var log = new LogDiv('state-log');
log.log('начали...');
var xhr = new XMLHttpRequest();
xhr.open('GET', '/files/tutorial/ajax/xhr/digits', true);
xhr.onreadystatechange = function() {
log.log("readyState: " + this.readyState + ' responseText: ' + this.responseText);
};
xhr.send('');
Эти свойства содержат HTTP-статус ответа и его описание, например:
status | statusText |
---|---|
200 | OK |
404 | Not Found |
500 | Internal Server Error |
... | ... |
Когда ошибка не связана с кодом ответа сервера (например, не удалось соединение), свойство status
равно нулю, а в statusText
— пустая строка.
Хорошей практикой является обязательная проверка на ошибку сравнением status!=200
. Например, в функции голосования:
function vote(outputElem) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/files/tutorial/ajax/xhr/vote', true);
xhr.onreadystatechange = function() {
if (xhr.readyState != 4) return;
if (xhr.status != 200) {
// status=0 при ошибках сети, иначе status=HTTP-код ошибки
alert('Ошибка ' + xhr.status + ': ' + xhr.statusText;
return;
}
// обработать результат
outputElem.innerHTML = xhr.responseText;
}
outputElem.innerHTML = '...';
xhr.send(null);
}
После завершения запроса становится доступно свойство responseText
, которое содержит текст ответа сервера. В современных браузерах оно доступно даже при неоконченном запросе и содержит текст, полученный к текущему моменту.
Если сервер прислал HTML/XML с Content-Type: text/xml
, то браузер превращает его в полноценный документ и записывает в responseXML
. По такому документу можно производить XPath-запросы, делать XSLT-преобразования и т.п.
Например:
var xhr = new XMLHttpRequest();
xhr.open('GET', '/files/tutorial/ajax/xhr/xml', true);
xhr.onreadystatechange = function() {
if (this.readyState != 4) return;
alert(this.responseText);
// responseXML содержит полноценный XML-документ
var author = this.responseXML.getElementsByTagName('author')[0];
alert(author.innerHTML); // undefined: в XML свойство innerHTML не работает!
alert(author.firstChild.data); // "Айн Рэнд", получили через DOM
}
xhr.send('');
Код на сервере:
res.writeHead(200, {'Content-Type': 'text/xml'});
res.end('<book><author>Айн Рэнд</author><title>Атла́нт расправил плечи</title></book>');
Content-Type важен для responseXML
.
Самое важное здесь — заголовок Content-Type: text/xml
.
Если его нет, то браузер не станет обрабатывать ответ как XML, и свойство responseXML
будет пустым.
Для работы с заголовками есть 3 метода:
setRequestHeader(name, value)
Устанавливает заголовок name
запроса со значением value
. Если заголовок с таким name уже есть — он заменяется.
Например:
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
Нельзя установить заголовки, которые контролирует браузер, например Referer
или Host
и ряд других (полный список тут).
Это ограничение существует в целях безопасности и для контроля корректности запроса.
Особенностью XMLHttpRequest
является то, что отменить setRequestHeader
невозможно.
Повторные вызовы добавляют информацию к заголовку:
xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');
// в результате будет заголовок:
// X-Auth: 123, 456
getResponseHeader(name)
Возвращает значение заголовка ответа name
, кроме Set-Cookie и Set-Cookie2.
Например:
xhr.getResponseHeader('Content-Type') == 'text/plain'
getAllResponseHeaders() Возвращает все заголовки ответа, кроме Set-Cookie и Set-Cookie2. Заголовки возвращаются в виде единой строки, например:
Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT
Между заголовками стоит перевод строки в два символа "\r\n"
(не зависит от ОС), значение заголовка отделено двоеточием с пробелом ": "
. Этот формат задан стандартом.
Таким образом, если хочется получить объект с парами заголовок-значение, то эту строку необходимо разбить и обработать.
Результат запроса XMLHttpRequest
, как и обычная страница, может быть закэширован браузером.
IE<10 автоматически кэширует ответы, не снабжённые антикэш-заголовком.
Это опасно, поскольку может поломать интерфейс. Тем более, что другие браузеры этого не делают. Например, если вы кликните на кнопку «Голосовать», то в IE запрос будет сделан только один раз, повторные клики возьмут результат из кэша.
Чтобы этого избежать, сервер должен добавить в ответ соответствующие антикэш-заголовки, например:
Cache-Control: no-cache
Альтернативный вариант — добавить в URL запроса случайный параметр, предотвращающий кэширование.
Например:
xhr.open('GET', '/xhr/vote?r=' + Math.random(), ...)
Внимание, в XMLHttpRequest
сломано кэширование!
Вообще, если механизм кэширования обычных страниц через HTTP-заголовки хорошо проработан, то при кешировании XMLHttpRequest
многие заголовки игнорируются или работают некорректно, поэтому полагаться на них не следует.
Это касается Cache-Control
, Last-Modified
, Expires
и других.
А чтобы не быть голословным — вот отличный набор онлайн-тестов, которые проверят поведение вашего браузера: XMLHTTPREQUEST CACHING TESTS.
Если вы хотите использовать браузерные кэширующие заголовки в сочетании с XMLHttpRequest
— убедитесь, что они поддерживаются браузерами.
Максимальную продолжительность запроса можно задать свойством timeout
:
xhr.timeout = 30000; // 30 секунд (в миллисекундах)
При превышении этого времени запрос будет оборван и сгенерировано событие ontimeout
:
xhr.ontimeout = function() {
alert('Извините, запрос превысил максимальное время');
}
responseText
.abort()
.Звёздочкой отмечены события, которые поддерживаются начиная в IE8 объектом XDomainRequest.
Типовой код для запроса XMLHttpRequest
:
var xhr = new XMLHttpRequest();
xhr.open('GET', '/my/url', true);
xhr.onreadystatechange = function() {
if (this.readyState != 4) return;
// по окончании запроса доступны:
// status, statusText
// responseText, responseXML (при content-type: text/xml)
if (this.status != 200) {
// обработать ошибку
return;
}
// получить результат из this.responseText или this.responseXML
}
xhr.send('');
Основные методы:
open(method, url, async, user, password)
send(body)
abort(body)
setRequestHeader(name, value)
getResponseHeader(name)
getAllResponseHeaders()
Основные свойства:
responseText
responseXML
status
statusText
Кроме того, есть особенности при работе с кэшированием: