Конспект JS-course

XMLHttpRequest

Источник: http://learn.javascript.ru/ajax-xmlhttprequest

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

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

Пример использования

Рассмотрим пример с кнопкой для голосования.

Внутри он состоит из трёх частей:

  1. Кнопка:
<button onclick="vote(this)">Голосовать</button>
  1. Функция голосования:

     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)
      }
    
  2. Серверный скрипт. В данном случае он выглядит так:
    res.end('Ваш голос принят: ' + new Date());
    

Разберём функцию голосования по шагам:

  1. Создаёт экземпляр встроенного объекта XMLHttpRequest. Этот объект предназначен для отправки запроса на сервер.
  2. Объект xhr конфигурируется: нам нужен GET-запрос на vote. На этом этапе соединение с сервером ещё не открыто.
  3. Продолжаем конфигурацию: обработчик xhr.onreadystatechange задаёт, что делать при ответе сервера.
    • Запрос проходит разные стадии обработки, определяемые по номеру xhr.readyState. Нас интересует только 4 (запрос завершён).
    • При этом свойство xhr.responseText содержит текст ответа сервера.
  4. Теперь, когда обработчик назначен, URL и остальные параметры указаны, отсылаем запрос xhr.send(null).

Далее мы посмотрим методы и события более подробно.

Методы open, send и abort

Эти три метода управляют основным потоком запроса:

open(method, URL, async, user, password)

Задаёт основные параметры запроса:

  • method — HTTP-метод. Как правило, используется GET либо POST, хотя доступны и более экзотические, вроде TRACE/DELETE/PUT и т.п.
  • URL — адрес запроса. Можно использовать не только HTTP/HTTPS, но и другие протоколы, например ftp:// и file://. При этом есть ограничения безопасности, называемые «Same Origin Policy»: запрос со страницы можно отправлять только на тот же протокол://домен:порт, с которого она пришла.
  • async — если установлено в false, то запрос производится синхронно, если true — асинхронно.
  • user, password — логин и пароль для HTTP-авторизации. Обязательны только первые два аргумента. Метод open сам по себе не открывает соединение, это делает 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

Событие 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; // запрос ещё не завершён

  // .. обработать завершение запроса, проверить ошибки
}

Остальные состояния:

  • В Firefox работают хорошо.
  • В IE, Opera не работают, браузеры пропускают состояния.
  • В Chrome частично работают, но браузер буферизует ответ, вызывая 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('');

Свойства status и statusText

Эти свойства содержат 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 и responseXML

После завершения запроса становится доступно свойство 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

Между заголовками стоит перевод строки в два символа &quot;\r\n&quot; (не зависит от ОС), значение заголовка отделено двоеточием с пробелом &quot;: &quot;. Этот формат задан стандартом. Таким образом, если хочется получить объект с парами заголовок-значение, то эту строку необходимо разбить и обработать.

Кэширование

Результат запроса 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

Максимальную продолжительность запроса можно задать свойством timeout:

xhr.timeout = 30000; // 30 секунд (в миллисекундах)

При превышении этого времени запрос будет оборван и сгенерировано событие ontimeout:

xhr.ontimeout = function() {
  alert('Извините, запрос превысил максимальное время');
}

Другие события загрузки

  • onerror* Ошибка при выполнении запроса.
  • onload* Запрос успешно завершён.
  • onprogress* Браузер получил очередной пакет данных. Можно прочитать текущие полученные данные в responseText.
  • onabort Запрос отменён вызовом abort().
  • onloadstart Запрос начат.
  • onloadend Запрос окончен, возможно с ошибкой.

Звёздочкой отмечены события, которые поддерживаются начиная в 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

Кроме того, есть особенности при работе с кэшированием:

  • Кэширующие загловки работают плохо.
  • IE кэширует ответ, если у него нет явного антикэш-заголовка.