XMLHttpRequest
– це вбудований об’єкт браузера, який дозволяє виконувати HTTP-запити за допомогою JavaScript.
Попри те, що в назві є слово “XML”, він може працювати з будь-якими даними, а не лише у форматі XML. Ми можемо завантажувати з/на сервер файли, відстежувати прогрес та багато іншого.
Нині існує інший, більш сучасний метод fetch
, який, у більшості випадків, замінив XMLHttpRequest
.
У сучасній веб-розробці XMLHttpRequest
використовується з трьох причин:
- З історичних причин: нам потрібно підтримувати наявні скрипти які використовують
XMLHttpRequest
. - Нам потрібно підтримувати старі браузери і ми не хочемо використовувати поліфіли (наприклад, щоб зменшити кількість коду).
- Нам потрібен функціонал, який
fetch
ще не підтримує, наприклад відстежування ходу завантаження.
Щось з цього звучить знайомо? Якщо так, то продовжуйте знайомство з XMLHttpRequest
. В іншому випадку можливо є сенс одразу перейти до Fetch.
Основи
XMLHttpRequest має два режими роботи: синхронний і асинхронний.
Давайте спочатку розглянемо асинхронний, оскільки він використовується в більшості випадків.
Щоб зробити запит, нам потрібно виконати 3 кроки:
-
Створити
XMLHttpRequest
:let xhr = new XMLHttpRequest();
Конструктор не має аргументів.
-
Ініціалізувати його, зазвичай це роблять відразу після
new XMLHttpRequest
:xhr.open(method, URL, [async, user, password])
Цей метод приймає основні параметри запиту:
method
– HTTP-метод. Зазвичай"GET"
або"POST"
.URL
– URL для запиту, зазвичай це рядок, але може бути і об’єктом URL.async
– якщо явно встановлено значенняfalse
, тоді запит буде синхронним, ми розглянемо це трохи пізніше.user
,password
– логін та пароль для базової HTTP аутентифікації (якщо потрібно).
Зверніть увагу, що виклик метода
open
, всупереч його назві, не відкриває з’єднання. Він лише налаштовує запит, але мережева активність починається лише після виклику методаsend
. -
Надіслати запит.
xhr.send([body])
Цей метод відкриває з’єднання та надсилає запит на сервер. Необов’язковий параметр
body
містить тіло запиту.Деякі HTTP-методи запиту, такі як
GET
, не мають тіла. А деякі з них, наприкладPOST
, використовуютьbody
для завантаження даних на сервер. Ми побачимо приклади цього пізніше. -
Прослуховувати події
xhr
.Найчастіше використовуються ці три події:
load
– спрацьовує коли запит завершено (навіть якщо статус HTTP дорівнює 400 або 500) і відповідь сервера повністю завантажено.error
– спрацьовує коли запит не може бути виконано, наприклад мережа не працює або задана не коректна URL-адреса.progress
– періодично спрацьовує під час завантаження відповіді, та повідомляє, скільки байтів було завантажено.
xhr.onload = function() { alert(`Завантажено: ${xhr.status} ${xhr.response}`); }; xhr.onerror = function() { // спрацьовує лише в тому випадку, якщо запит взагалі неможливо зробити alert(`Мережева помилка`); }; xhr.onprogress = function(event) { // спрацьовує періодично // event.loaded - скільки байтів було завантажено // event.lengthComputable = true, якщо сервер надіслав заголовок Content-Length // event.total - загальна кількість байтів (якщо сервер надіслав заголовок Content-Length) alert(`Отримано ${event.loaded} з ${event.total}`); };
Ось повний приклад. Наведений нижче код завантажує /article/xmlhttprequest/example/load
із сервера та показує хід завантаження:
// 1. Створюємо новий об’єкт XMLHttpRequest
let xhr = new XMLHttpRequest();
// 2. Налаштовуємо його: GET-запит для URL-адреси /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');
// 3. Відправляємо запит мережею
xhr.send();
// 4. Код нижче буде виконано після отримання відповіді
xhr.onload = function() {
if (xhr.status != 200) { // аналізуємо HTTP-статус відповіді
alert(`Помилка ${xhr.status}: ${xhr.statusText}`); // наприклад 404: Не знайдено
} else { // виводимо результат
alert(`Запит завершено, отримано ${xhr.response.length} байт`); // властивість `xhr.response` містить відповідь сервера
}
};
xhr.onprogress = function(event) {
if (event.lengthComputable) {
alert(`Отримано ${event.loaded} із ${event.total} байт`);
} else {
alert(`Отримано ${event.loaded} байт`); // якщо сервер не надіслав заголовок Content-Length
}
};
xhr.onerror = function() {
alert("Не вдалося виконати запит");
};
Після відповіді сервера, ми можемо отримати результат у таких властивостях xhr
:
status
- Код HTTP статусу (число):
200
,404
,403
і так далі, може бути0
у разі помилки не пов’язаної з HTTP. statusText
- Статусне повідомлення HTTP (рядок): зазвичай
OK
для200
,Not Found
для404
,Forbidden
для403
тощо. response
(старі скрипти можуть використовуватиresponseText
)- Тіло відповіді сервера.
Ми також можемо вказати час очікування відповіді за допомогою відповідної властивості:
xhr.timeout = 10000; // час очікування в мс, 10 секунд
Якщо запит не завершився протягом заданого часу, він скасовується і спрацьовує подія timeout
.
Щоб додати параметри до URL-адреси, наприклад ?name=value
, і забезпечити належне кодування, ми можемо використовувати об’єкт URL:
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
Тип відповіді
Ми можемо встановити очікуваний формат відповіді сервера за допомогою властивості xhr.responseType
:
""
(за замовчуванням) – отримати як рядок,"text"
– отримати як рядок,"arraybuffer"
– отримати якArrayBuffer
(для двійкових даних див. розділ ArrayBuffer, бінарні масиви),"blob"
– отримати якBlob
(для двійкових даних див. розділ Blob),"document"
– отримати як XML-документ (може використовувати XPath та інші методи XML) або HTML-документ (на основі типу MIME для отриманих даних),"json"
– отримати як JSON (автоматичний аналіз).
Наприклад, отримаймо відповідь у форматі JSON:
let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/example/json');
xhr.responseType = 'json';
xhr.send();
// тіло відповіді {"message": "Привіт, світ!"}
xhr.onload = function() {
let responseObj = xhr.response;
alert(responseObj.message); // Привіт, світ!
};
У старих скриптах ви також можете знайти властивості xhr.responseText
і навіть xhr.responseXML
.
Вони існують з історичних причин, щоб отримати рядок або XML-документ. Зараз ми повинні встановлювати формат у xhr.responseType
і отримувати відповідь через властивість xhr.response
, як показано вище.
Стани запиту
XMLHttpRequest
змінює свій стан в процесі виконання запиту. Поточний стан доступний у властивості xhr.readyState
.
Ось список усіх станів, згідно зі специфікацією:
UNSENT = 0; // початковий стан
OPENED = 1; // викликано метод open
HEADERS_RECEIVED = 2; // отримано заголовки відповіді
LOADING = 3; // завантажується відповідь (отримано пакет даних)
DONE = 4; // запит завершено
Стани об’єкту XMLHttpRequest
змінюються у порядку 0
→ 1
→ 2
→ 3
→ … → 3
→ 4
. Стан 3
повторюється щоразу, коли отримується пакет даних.
Ми можемо відстежувати їх за допомогою події readystatechange
:
xhr.onreadystatechange = function() {
if (xhr.readyState == 3) {
// завантаження
}
if (xhr.readyState == 4) {
// запит завершено
}
};
Ви можете натрапити на обробники події readystatechange
у дуже старому коді. Вони там з історичних причин, оскільки раніше не було події load
та інших. Нині замість цього використовують обробники подій load/error/progress
.
Скасування запиту
Ми можемо скасувати запит у будь-який час. Це можна зробити за допомогою метода xhr.abort()
:
xhr.abort(); // скасовуємо запит
Це ініціює подію abort
, а xhr.status
стає 0
.
Синхронні запити
Якщо в методі open
для третього параметра async
встановлено значення false
, то запит виконується синхронно.
Іншими словами, виконання JavaScript призупиняється в момент виклику метода send()
і відновлюється після отримання відповіді. Схожим чином поводяться команди alert
та prompt
.
Ось переписаний приклад, де 3-й параметр open
встановлено у false
:
let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/hello.txt', false);
try {
xhr.send();
if (xhr.status != 200) {
alert(`Error ${xhr.status}: ${xhr.statusText}`);
} else {
alert(xhr.response);
}
} catch(err) { // для оброки помилок використовуємо try...catch замість події onerror
alert("Не вдалося виконати запит");
}
Можливо це і виглядає прийнятно, але синхронні запити використовуються рідко, оскільки вони блокують виконання JavaScript на сторінці до моменту завершення завантаження. У деяких браузерах прокручування стає неможливим. Якщо синхронний запит займає занадто багато часу, браузер може запропонувати закрити «висячу» веб-сторінку.
Багато розширених можливостей XMLHttpRequest
, таких як запит до іншого домену або встановлення часу очікування відповіді, недоступні для синхронних запитів. Крім того, як бачите, немає індикаторів прогресу.
Через все це синхронні запити використовуються дуже рідко, майже ніколи. Тож ми більше не будемо їх розглядати.
HTTP-заголовки
XMLHttpRequest
дозволяє як надсилати власні заголовки, так і читати їх з відповіді.
Існує 3 методи для роботи з HTTP-заголовками:
setRequestHeader(name, value)
-
Встановлює заголовок запиту із заданими ім’ям
name
та значеннямvalue
.Наприклад:
xhr.setRequestHeader('Content-Type', 'application/json');
Обмеження заголовківДекількома заголовками керує виключно браузер, наприклад
Referer
іHost
. Повний їх список є у специфікації.XMLHttpRequest
не має права їх змінювати з метою безпеки користувача та коректності запиту.Видалити заголовок неможливоЩе одна особливість
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')
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"
(не залежить від ОС), тому ми можемо легко розділити його на окремі заголовки. Роздільником між ім’ям і значенням заголовка завжди є двокрапка з пробілом": "
. Це зафіксовано в специфікації.Отже, якщо ми хочемо отримати об’єкт з парами ім’я/значення, нам потрібно додати трохи JS.
Ось так (припускаючи, що якщо два заголовки мають однакову назву, то останній замінює попередній):
let headers = xhr .getAllResponseHeaders() .split('\r\n') .reduce((result, current) => { let [name, value] = current.split(': '); result[name] = value; return result; }, {}); // headers['Content-Type'] = 'image/png'
POST, FormData
Щоб зробити запит POST, ми можемо використати вбудований об’єкт FormData.
Синтаксис:
let formData = new FormData([form]); // створюємо об’єкт, він може бути заповнений з елемента <form>
formData.append(name, value); // додаємо поле
Ми створюємо об’єкт, за бажанням додаємо дані з форми, та якщо потрібно, додаємо поля за допомогою метода append
, а потім:
xhr.open('POST', ...)
– встановлюємо методPOST
.xhr.send(formData)
– та надсилаємо форму на сервер.
Наприклад:
<form name="person">
<input name="name" value="John">
<input name="surname" value="Smith">
</form>
<script>
// заповнюємо FormData даними із форми
let formData = new FormData(document.forms.person);
// додаємо ще одне поле
formData.append("middle", "Lee");
// відправляємо запит
let xhr = new XMLHttpRequest();
xhr.open("POST", "/article/xmlhttprequest/post/user");
xhr.send(formData);
xhr.onload = () => alert(xhr.response);
</script>
Форма надсилається з кодуванням multipart/form-data
.
Або, якщо нам більше подобається формат JSON, тоді використовуємо JSON.stringify
і надсилаємо дані як рядок.
Тільки не забудьте встановити заголовок Content-Type: application/json
, багато серверних фреймворків автоматично декодують JSON за його наявності:
let xhr = new XMLHttpRequest();
let json = JSON.stringify({
name: "John",
surname: "Smith"
});
xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
xhr.send(json);
Метод .send(body)
досить всеїдний. Він може надсилати майже будь-які дані у body
, включаючи об’єкти Blob
та BufferSource
.
Хід завантаження
Подія progress
спрацьовує лише на етапі завантаження даних з сервера.
Тобто якщо ми відправляємо POST
запит, то XMLHttpRequest
спочатку завантажує наші дані (тіло запиту) на сервер, а потім завантажує відповідь з сервера.
Якщо ми завантажуємо на сервер щось велике, то ми, звичайно, більше зацікавлені у відстеженні прогресу завантаження на сервер. Але xhr.onprogress
тут нам не допоможе.
Існує ще один об’єкт без методів, призначений виключно для відстеження подій завантаження на сервер: xhr.upload
.
Він генерує події, подібні до xhr
, але xhr.upload
ініціює їх виключно під час завантаження на сервер:
loadstart
– розпочато завантаження на сервер.progress
– спрацьовує періодично під час завантаження.abort
– завантаження перервано.error
– помилка не пов’язана з HTTP.load
– завантаження успішно завершено.timeout
– час очікування завантаження минув (якщо встановлено властивістьtimeout
).loadend
– завантаження завершено (успішно або з помилкою).
Приклад обробників:
xhr.upload.onprogress = function(event) {
alert(`Завантажено на сервер ${event.loaded} із ${event.total} байтів`);
};
xhr.upload.onload = function() {
alert(`Завантаження на сервер успішно завершено.`);
};
xhr.upload.onerror = function() {
alert(`Сталася помилка під час завантаження: ${xhr.status}`);
};
Ось приклад із реального життя: завантаження файлу на сервер з індикацією прогресу:
<input type="file" onchange="upload(this.files[0])">
<script>
function upload(file) {
let xhr = new XMLHttpRequest();
// відстежуємо хід завантаження на сервер
xhr.upload.onprogress = function(event) {
console.log(`Завантажено ${event.loaded} із ${event.total}`);
};
// відстежуємо завершення: успішне чи ні
xhr.onloadend = function() {
if (xhr.status == 200) {
console.log("успішно");
} else {
console.log("помилка " + this.status);
}
};
xhr.open("POST", "/article/xmlhttprequest/post/upload");
xhr.send(file);
}
</script>
Запити на інші джерела
XMLHttpRequest
може робити запити інші джерела (сайти) використовуючи ту саму політику CORS, що й fetch.
Так само як і fetch
, він за замовчуванням не надсилає іншим джерелам заголовки HTTP-авторизації та cookie
. Щоб увімкнути їх, встановіть для xhr.withCredentials
значення true
:
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('POST', 'http://anywhere.com/request');
...
Дивіться розділ Fetch: Запити між різними джерелами, щоб дізнатися більше про заголовки які використовуються для запитів на інші джерела.
Підсумки
Типовий код GET-запиту з використанням XMLHttpRequest
:
let xhr = new XMLHttpRequest();
xhr.open('GET', '/my/url');
xhr.send();
xhr.onload = function() {
if (xhr.status != 200) { // HTTP помилка?
// оброблюємо помилку
alert( 'Помилка: ' + xhr.status);
return;
}
// отримуємо відповідь з властивості xhr.response
};
xhr.onprogress = function(event) {
// відстежуємо прогрес
alert(`Завантажено ${event.loaded} із ${event.total}`);
};
xhr.onerror = function() {
// обробляємо помилку не пов’язану з HTTP (наприклад, якщо мережа не працює)
};
Насправді подій більше, сучасна специфікація перелічує їх (у порядку життєвого циклу):
loadstart
– запит почався.progress
– надійшов пакет даних відповіді, все тіло відповіді на даний момент знаходиться у властивостіresponse
.abort
– запит було скасовано викликомxhr.abort()
.error
– сталася помилка підключення, наприклад неправильне доменне ім’я. Не спрацьовує для HTTP-помилок, таких як 404.load
– запит успішно завершено.timeout
– запит було скасовано через тайм-аут (тільки якщо він був встановлений).loadend
– спрацьовує після подійload
,error
,timeout
абоabort
.
Події error
, abort
, timeout
, та load
є взаємовиключними. Може спрацювати лише одна з них.
Найчастіше використовувані події – це завершення завантаження (load
), помилка завантаження (error
), або ми можемо використовувати один обробник loadend
і перевірити властивості об’єкта запиту xhr
, щоб побачити, що сталося.
Також ми розглянули іншу подію: readystatechange
. Вона з’явилася дуже давно, ще до того, як було завершено специфікацію. Нині нема потреби її використовувати, адже є новіші події, але її часто можна зустріти в старих скриптах.
Якщо ж нам потрібно відстежувати прогрес завантаження на сервер, тоді можна прослуховувати ті самі події на об’єкті xhr.upload
.