XMLHttpRequest – это API, который предоставляет клиенту функциональность для обмена данными между клиентом и сервером (предоставляет простой способ получения данных по ссылке без перезагрузки страницы). Это позволяет обновлять только часть веб-страницы, не прерывая действий пользователя. Метод XMLHttpRequest() используется в AJAX запросах и, особенно, в single-page приложениях.
Замечания:
- XMLHttpRequest может работать с любыми данными, а не только с XML.
- На сегодняшний день не обязательно использовать XMLHttpRequest(), так как существует более современный метод fetch().
Режимы работы XMLHttpRequest:
- синхронный;
- асинхронный.
Синхронные запросы в основном потоке могут нарушить работу пользователя, их следует избегать. Фактически большинство браузеров полностью отказались от поддержки синхронных XHR в основном потоке. Синхронные запросы можно использовать в Worker.
Асинхронный режим работы XMLHttpRequest
Чтобы сделать запрос к серверу и получить ответ, необходимо:
- Создать объект XMLHttpRequest.
- Инициализировать запрос, используя метод open().
- Послать запрос, используя метод send().
- Слушать следующие события на XMLHttpRequest, чтобы получить ответ:
- load – происходит, когда получен какой-либо ответ (включая ответы с HTTP-ошибкой, например, 404);
- error – когда запрос не может быть выполнен (например, нет соединения или невалидный URL);
- progress – происходит периодически во время загрузки ответа, сообщает о прогрессе.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function getXHRequest() { let xhr = new XMLHttpRequest(); // создаем запрос на сервер xhr.open("GET", "https://jsonplaceholder.typicode.com/posts"); // инициализируем запрос на сайт xhr.send(); // отправляем запрос // слушаем событие onload и выводим результат xhr.onload = ()=>{ console.log(xhr); // XMLHttpRequest{} console.log(xhr.status); // 200 console.log(xhr.response); // ответ console.log(JSON.stringify(xhr.response)); // ответ в виде строки } // ИЛИ xhr.addEventListener("load",()=>{ console.log(xhr); // XMLHttpRequest{} console.log(xhr.status); // 200 console.log(xhr.response); // ответ console.log(JSON.stringify(xhr.response)); // ответ в виде строки }) } getXHRequest(); |
Объект XMLHttpRequest
Объект XMLHttpRequest создается с помощью конструкции new:
1 |
let xhr = new XMLHttpRequest(); // у конструктора нет аргументов |
Новый объект XMLHttpRequest должен быть подготовлен вызовом функции open() перед вызовом send() для отправки запроса на сервер.
Метод XMLHttpRequest.open()
Метод XMLHttpRequest.open() (https://developer.mozilla.org) инициализирует новый запрос или повторно инициализирует уже созданный запрос:
1 |
xhr.open(method, URL, [async, user, password]) |
Этот метод обычно вызывается сразу после new XMLHttpRequest. В него передаются основные параметры запроса:
- method - для HTTP-запроса используются такие методы как "GET", "POST", "PUT", "DELETE", и т. д. (игнорируется для URL, отличных от HTTP(S)-запросов);
- url - DOMString, представляет URL для отправки запроса;
- async (необязательный, по умолчанию true) - логический параметр, указывает выполнять операцию асинхронно или нет:
- если false, метод send() не возвращается, пока не будет получен ответ;
- если true, уведомление о получении ответа осуществляется с помощью обработчика события (должен быть true, если атрибут multipart равен true, иначе будет выброшено исключение).
- user (необязательный, по умолчанию null) - имя пользователя, использующееся для аутентификации;
- password (необязательный, по умолчанию null) - пароль, использующийся для аутентификации.
Особенности метода XMLHttpRequest.open():
- Вызов open() не открывает соединение (он лишь конфигурирует запрос), отсылка на сервер - после вызова send().
- Вызов метода XMLHttpRequest.open() для уже активного запроса (для которого уже был вызван open()) эквивалентно вызову abort().
https://developer.mozilla.org/ru/docs/Web/HTTP/Methods
- GET – получить определённый ресурс (например, HTML-файл, содержащий информацию о товаре или список товаров);
- HEAD – получить метаданные об определённом ресурсе без получения содержания (например, вы можете использовать запрос HEAD, чтобы узнать, когда ресурс в последний раз обновлялся, и только потом использовать (более «затратный») запрос GET, чтобы загрузить сам ресурс, если он был изменён);
- POST – создать новый ресурс (например, добавить новую статью или новый контакт в базу данных);
- PUT – обновить существующий ресурс (или создать новый, если таковой не существует). Всегда идемпотентен. Операция считается идемпотентной, если её многократное выполнение приводит к тому же результату, что и однократное выполнение (например, если автоинкрементное поле является важной частью ресурса, то PUT перезапишет его (т.к. он перезаписывает всё), а вот PATCH (см. ниже) может и не перезаписать;
- DELETE – удалить определённый ресурс;
- TRACE - выполняет проверку обратной связи по пути к целевому ресурсу, предоставляя полезный механизм отладки;
- OPTIONS - используется для описания параметров соединения с целевым ресурсом;
- CONNECT - запускает двустороннюю связь с запрошенным ресурсом (метод можно использовать для открытия туннеля);
- PATCH – частично изменяет ресурс (при этом, в отличие от PUT может как быть идемпотентным, так и не быть, который всегда идемпотентен.
- Операция считается идемпотентной, если её многократное выполнение приводит к тому же результату, что и однократное выполнение (например, если автоинкрементное поле является важной частью ресурса, то PUT перезапишет его (т.к. он перезаписывает всё), а вот PATCH (см. ниже) может и не перезаписать.
Передача URL с параметрами
Отсюда https://learn.javascript.ru/xmlhttprequest
Чтобы добавить к URL параметры, вида ?name=value, и корректно закодировать их, можно использовать объект URL:
1 2 3 4 5 |
let url = new URL('https://google.com/search'); url.searchParams.set('q', 'test me!'); // параметр 'q' закодирован xhr.open('GET', url); // https://google.com/search?q=test+me%21 |
Метод XMLHttpRequest.send()
Метод XMLHttpRequest.send() (https://developer.mozilla.org/) устанавливает соединение и отсылает запрос к серверу:
- eсли запрос асинхронный (каким он является по умолчанию), то возврат из данного метода происходит сразу после отправления запроса;
- если запрос синхронный, то метод возвращает управление только после получения ответа.
1 |
xhr.send([body]) |
где body (необязательный, по умолчанию null) - данные, отправляемые в запросе, например:
- Document, в этом случае данные будут сериализованы перед отправкой;
- BodyInit, которые, согласно спецификации Fetch, могут быть Blob, BufferSource (en-US), FormData, URLSearchParams, ReadableStream (en-US), или объектом USVString.
Лучший способ передать двоичные данные (например при загрузке файлов) - это использование ArrayBufferView или Blobs в сочетании с методом send().
Особенности метода XMLHttpRequest.send():
- Если метод запроса GET или HEAD, то аргументы игнорируются и тело запроса устанавливается в null.
- Если заголовок Accept не был задан с помощью setRequestHeader(), будет отправлено значение Accept по умолчанию */*.
Прослушивание событий XMLHttpRequest
Для получения ответа от сервера необходимо слушать события на xhr, из которых три наиболее используемых:
- load – происходит, когда получен какой-либо ответ, включая ответы с HTTP-ошибкой (например, 404);
- error – когда запрос не может быть выполнен (например, нет соединения или ошибка в домене URL);
- progress – происходит периодически во время загрузки ответа, сообщает о прогрессе.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let url = "https://jsonplaceholder.typicode.com/users"; let xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.onload = function (e) { if (xhr.status != 200) { // анализируем HTTP-статус ответа, если статус не 200, то произошла ошибка при передаче данных console.log(`Ошибка ${xhr.status}`); } console.log(`Загружено: ${e.loaded} bytes`); }; xhr.onerror = function (e) { console.log(`Сетевая ошибка (подключение или домен): ${e.loaded} bytes transferred`); }; xhr.send(); |
Для событий доступны следующие свойства (только для чтения):
-
- lengthComputable - логический флаг, указывающий поддается ли расчету общая работа, которую необходимо выполнить базовым процессом, и объем уже выполненной работы (другими словами, измеряем ли прогресс процесса или нет; true, если сервер присылает заголовок Content-Length);
- loaded - 64-битное целое число без знака, указывающее объем работы, уже выполненной базовым процессом (при загрузке ресурса с использованием HTTP учитывается только тело HTTP-сообщения и не включаются заголовки и другие служебные данные);
- total - 64-битное целое число без знака, представляющее общий объем работы, которую выполняет базовый процесс (при загрузке ресурса с использованием HTTP, это - Content-Length (размер тела сообщения), не включает заголовки и другие служебные данные; только если lengthComputable равно true).
После ответа сервера (событие load) может получить результат запроса в следующих свойствах:
- status - rод состояния HTTP (число: 200, 404, 403 и так далее, может быть 0 в случае, если ошибка не связана с HTTP);
- statusText - сообщение о состоянии ответа HTTP (строка: обычно OK для 200, Not Found для 404, Forbidden для 403, и так далее);
- response (в старом коде - responseText) - тело ответа сервера;
- responseType - ожидаемый тип ответа сервера (позволяет автору изменить тип ответа):
- "" (по умолчанию) – строка;
- "text" – строка;
- "arraybuffer" – ArrayBuffer (для бинарных данных);
- "blob" – Blob (для бинарных данных);
- "document" – XML-документ (может использовать XPath и другие XML-методы);
- "json" – JSON (парсится автоматически, пример здесь).
Можно также указать таймаут (промежуток времени в миллисекундах), в течение которого мы готовы ждать ответ:
1 |
xhr.timeout = 10000; // в миллисекундах, т.е. 10 секунд |
Если запрос не успевает выполниться в установленное время, то он прерывается, и происходит событие timeout.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
"use strict"; // Получить пользователей (users) от сервера https://jsonplaceholder.typicode.com. let users; let urlAPI = "https://jsonplaceholder.typicode.com"; // Создаем запрос и оборачиваем его в функцию function generateXHR(callback) { let xhr = new XMLHttpRequest(); xhr.open("GET", `${urlAPI}/users`); xhr.addEventListener("load", () => { // стрелочная функция внутри load users = JSON.parse(xhr.response); // преобразуем JSON в объект (массив) callback(users); // замыкание }); xhr.send(); } // при вызове generateXHR() определяем функцию callback, // которая получает users и возвращает результат (например, выводит в консоль) generateXHR((users) => { console.log(users); // (10) [{…}, {…}, … , {…}, {…}, {…}] }); |
Состояния асинхронного запроса XMLHttpRequest
У XMLHttpRequest есть состояния, которые меняются по мере его выполнения.
Текущее состояние запроса можно посмотреть в свойстве xhr.readyState.
Список всех состояний, указанных в спецификации:
- UNSENT = 0; (исходное состояние);
- OPENED = 1; (вызван метод open);
- HEADERS_RECEIVED = 2; (получены заголовки ответа);
- LOADING = 3; (ответ в процессе передачи, данные частично получены);
- DONE = 4; (запрос завершён).
Состояния объекта XMLHttpRequest меняются в таком порядке: 0 → 1 → 2 → 3 → … → 3 → 4 (состояние 3 повторяется каждый раз, когда получена часть данных).
Изменения в состоянии объекта запроса генерируют событие readystatechange (устар.):
1 2 3 4 5 6 7 8 |
xhr.onreadystatechange = function() { if (xhr.readyState == 3) { // загрузка } if (xhr.readyState == 4) { // запрос завершён } }; |
Из-за появления событий load, error, progress можно считать, что событие readystatechange устарело.
Отмена асинхронного запроса XMLHttpRequest
Если мы передумали делать запрос, можно отменить его вызовом метода abort():
1 |
xhr.abort(); // завершить запрос |
При этом генерируется событие abort, а xhr.status устанавливается в 0.
Синхронные запросы
Если в методе open() третий параметр async установлен на false, запрос выполняется синхронно: выполнение JavaScript останавливается на send() и возобновляется после получения ответа (пример здесь).
Синхронные запросы используются редко, так как они блокируют выполнение JavaScript до тех пор, пока загрузка не завершена.
Работа XMLHttpRequest с HTTP-заголовками
на https://developer.mozilla.org/ru/docs/Web/HTTP/Headers
HTTP-заголовки сопровождают обмен данными по протоколу HTTP и позволяют клиенту и серверу отправлять дополнительную информацию с HTTP запросом или ответом. Они могут содержать описание данных и информацию, необходимую для взаимодействия между клиентом и сервером.
В HTTP-заголовке содержится не чувствительное к регистру название, а затем после (:) непосредственно значение. Пробелы перед значением игнорируются.
Заголовки и их статусы перечислены в реестре IANA, который постоянно обновляется.
Заголовки могут быть сгруппированы по следующим контекстам:
- основной - применяется как к запросам, так и к ответам, но не имеет отношения к данным, передаваемым в теле;
- запроса - содержит больше информации о ресурсе, который нужно получить, или о клиенте, запрашивающем ресурс;
- ответа - содержит дополнительную информацию об ответе, например его местонахождение, или о сервере, предоставившем его;
- сущности - содержит информацию о теле ресурса, например его длину содержимого или тип MIME.
XMLHttpRequest умеет как передавать заголовки в запросе, так и читать присланные в ответ.
Некоторые заголовки управляются исключительно браузером (например Referer или Host, и др.). XMLHttpRequest не разрешено изменять их ради безопасности пользователей и для обеспечения корректности HTTP-запроса.
Методы XMLHttpRequest для работы с HTTP-заголовками:
- setRequestHeader(name, value) - устанавливает заголовок запроса с именем name и значением value.
- getResponseHeader(name) - возвращает значение заголовка ответа name (кроме Set-Cookie и Set-Cookie2).
Метод setRequestHeader(name, value) устанавливает заголовок запроса с именем name и значением value, например:
1 |
xhr.setRequestHeader('Content-Type', 'application/json'); |
Отменить setRequestHeader невозможно: если заголовок определён, то его нельзя снять. Повторные вызовы лишь добавляют информацию к заголовку, а не перезаписывают его.
Например:
1 2 3 4 5 |
xhr.setRequestHeader('X-Auth', '123'); xhr.setRequestHeader('X-Auth', '456'); // заголовок получится такой: // X-Auth: 123, 456 |
Метод getResponseHeader(name) - возвращает значение заголовка ответа name (кроме Set-Cookie и Set-Cookie2), например:
1 |
xhr.getResponseHeader('Content-Type') |
Метод getAllResponseHeaders() - возвращает все заголовки ответа (кроме Set-Cookie и Set-Cookie2) в виде единой строки, например:
1 2 3 4 |
Cache-Control: max-age=31536000 Content-Length: 4260 Content-Type: image/png Date: Sat, 08 Sep 2012 16:53:16 GMT |
Значение заголовка всегда отделено двоеточием с пробелом ":" (этот формат задан стандартом).
Между заголовками всегда стоит перевод строки в два символа "\r\n" (независимо от ОС), так что мы можем легко разделить их на отдельные заголовки, например так (если два заголовка имеют одинаковое имя, то последний перезаписывает предыдущий):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// получим все заголовки запроса в виде строки let headers = xhr.getAllResponseHeaders(); console.log(`headers ${headers} `); // получим все заголовки запроса в виде объекта headers = xhr .getAllResponseHeaders() .split("\r\n") .reduce((result, current) => { let [name, value] = current.split(": "); result[name] = value; return result; }, {}); console.log(headers); |
Запрос POST (объект FormData)
подробнее https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
HTTP-метод POST предназначен для отправки данных на сервер. Тип тела запроса указывается в заголовке Content-Type.
Отличие PUT от POST:
- PUT является идемпотентным: повторное его применение даёт тот же результат, что и при первом применении (то есть у метода нет побочных эффектов), тогда как повторный вызов одного и того же метода POST может иметь такие эффекты, как например, оформление одного и того же заказа несколько раз.
Запрос POST обычно отправляется через форму HTML и приводит к изменению на сервере. В этом случае тип содержимого выбирается путём размещения соответствующей строки в атрибуте enctype элемента <form> или formenctype атрибута элементов <input> или <button>:
- application/x-www-form-urlencoded: значения кодируются в парах с ключом, разделённых символом '&', с '=' между ключом и значением; не буквенно-цифровые символы - это причина, по которой данный тип не подходит для использования с двоичными данными (вместо этого используйте multipart/form-data);
- multipart/form-data: каждое значение посылается как блок данных ("body part"), с заданными пользовательским клиентом разделителем ("boundary"), разделяющим каждую часть (ключи задаются в заголовке Content-Disposition каждой части);
- text/plain - когда запрос POST отправляется с помощью метода, отличного от HTML-формы (например, через XMLHttpRequest, тело может принимать любой тип).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let xhr = new XMLHttpRequest(); xhr.open("POST", "https://jsonplaceholder.typicode.com/posts"); let body = { title: 'foo', body: 'bar', userId: 1, }; xhr.addEventListener("load", () => { console.log(JSON.parse(xhr.response)); }); xhr.setRequestHeader('Content-type', 'application/json; charset=UTF-8') xhr.send(JSON.stringify(body)); |
Чтобы сделать POST-запрос, мы можем использовать встроенный объект FormData.
Синтаксис:
1 2 |
let formData = new FormData([form]); // создаём объект, по желанию берём данные формы <form> formData.append(name, value); // добавляем поле |
Обычно форма отсылается в кодировке multipart/form-data.
Если нам больше нравится формат JSON, то используем JSON.stringify и отправляем данные как строку.
Важно не забыть поставить соответствующий заголовок Content-Type: application/json, многие серверные фреймворки автоматически декодируют JSON при его наличии:
1 2 3 4 5 6 7 8 9 10 11 |
let xhr = new XMLHttpRequest(); let json = JSON.stringify({ name: "Вася", surname: "Петров", }); xhr.open("POST", "/submit"); xhr.setRequestHeader("Content-type", "application/json; charset=utf-8"); xhr.send(json); |
Метод send(body) может отправить практически что угодно в body, включая объекты типа Blob и BufferSource.
Прогресс отправки (XMLHttpRequest.upload)
Событие progress срабатывает только на стадии загрузки ответа с сервера, но не показывает прогресс отправки данных на сервер.
Свойство XMLHttpRequest.upload возвращает объект XMLHttpRequestUpload, представляющий процесс загрузки, и к которому можно добавить следующие обработчики событий:
- onloadstart - начало загрузки;
- onprogress - передача данных;
- onabort - отмена загрузки;
- onerror - ошибка при загрузки;
- onload - загрузка завершилась;
- ontimeout - загрузка не завершилась ко времени, указанному автором;
- onloadend - загрузка завершилась (успешно или с ошибкой).
1 2 3 4 5 6 7 8 9 10 11 |
xhr.upload.onprogress = function (event) { console.log(`Отправлено ${event.loaded} из ${event.total} байт`); }; xhr.upload.onload = function () { console.log(`Данные успешно отправлены.`); }; xhr.upload.onerror = function () { console.log(`Произошла ошибка во время отправки: ${xhr.status}`); }; |
Запросы с XMLHttpRequest на другой источник
XMLHttpRequest может осуществлять запросы на другие сайты, используя ту же политику CORS, что и fetch.
Cross-Origin Resource Sharing (CORS) — механизм, использующий дополнительные HTTP-заголовки, чтобы дать возможность агенту пользователя получать разрешения на доступ к выбранным ресурсам с сервера на источнике (домене), отличном от того, что сайт использует в данный момент.
Агент пользователя делает запрос с другого источника (cross-origin HTTP request), если источник текущего документа отличается от запрашиваемого ресурса доменом, протоколом или портом.
Как и при работе с fetch, по умолчанию на другой источник не отсылаются куки и заголовки HTTP-авторизации. Чтобы это изменить, необходимо установить свойство xhr.withCredentials в true:
1 2 |
let xhr = new XMLHttpRequest(); xhr.withCredentials = true; |