XMLHttpReqest и fetch от Я
XMLHttpRequest
Объект XMLHttpRequest, или кратко — XHR, позволяет отправлять HTTP-запросы к серверу из JavaScript без перезагрузки страницы. Он работает с любыми данными, хотя слово “XML” (Extensible Markup Language, то есть «расширяемый язык разметки») вначале может смутить. Оно осталось в названии по историческим причинам и из-за сохранения обратной совместимости. Отправим запрос:
const xhr = new XMLHttpRequest();
// Конфигурируем запрос
xhr.open('GET', 'https://url/');
// Отсылаем запрос
xhr.send();
// Если код ответа сервера не 200, обработаем как ошибку
if (xhr.status === 200) {
console.log(xhr.responseText); // responseText — текст ответа
} else {
// Выведём результат
console.log(`Ответ от сервера: ${xhr.status} | ${xhr.statusText}`);
}
Конфигурировать XHR можно любым нужным для вас способом. Например, хотим отправить POST-запрос с различными заголовками и тело в виде JSON:
const xhr = new XMLHttpRequest();
xhr.open('POST', '/create');
// Выставляем заголовок
xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
xhr.send(JSON.stringify({data: 42}));
В теле запроса можно отправить даже файлы:
const xhr = new XMLHttpRequest();
xhr.open('POST', '/avatar');
const fileInput = document.querySelector('input[type=file]');
const file = fileInput.files[0].file;
const formdata = new FormData();
formdata.append('file', file);
xhr.send(formdata);
У XHR есть два режима работы: синхронный и асинхронный. В асинхронном режиме необходимо подписываться на события:
const xhr = new XMLHttpRequest();
xhr.open('GET', '/chats');
xhr.onreadystatechange = function() {
if (xhr.readyState === 3) {
// загрузка
}
if (xhr.readyState === 4) {
// запрос завершён
}
}
xhr.timeout = 5000; // 5 секунд (в миллисекундах)
xhr.send();
Значения readyState могут быть такими:
UNSENT = 0; // начальное состояние
OPENED = 1; // вызван open
HEADERS_RECEIVED = 2; // получены заголовки
LOADING = 3; // загружается тело
DONE = 4; // запрос завершён
Какие ещё события могут понадобиться:
loadstart
— запрос начат;progress
— браузер получил очередной пакет данных;abort
— запрос был отменён вызовом xhr.abort();error
— произошла ошибка;load
— запрос был успешно (без ошибок) завершён;loadend
— запрос был прекращён по таймауту;readystatechange
— запрос был завершён (успешно или неуспешно).
Чтобы сказать браузеру подцепить cookie во время запроса, достаточно выставить специальное свойство withCredentials в значение true:
const xhr = new XMLHttpRequest();
xhr.open('GET', '/request');
xhr.withCredentials = true;
xhr.send();
XHR даёт возможность отменять запросы. Например, если передумали, прошло много времени, или пользователь ушёл со страницы и запрос больше не нужен. Это можно сделать с помощью метода abort:
xhr.abort();
Fetch
Метод fetch — это XMLHttpRequest
нового поколения. Он предоставляет улучшенный интерфейс для осуществления запросов к серверу: как по части возможностей и контроля над происходящим, так и по синтаксису, поскольку он построен на промисах.
Синтаксис метода:
const fetchPromise = fetch(url, [options]);
Второй аргумент (options)
задаёт все свойства запроса — от метода до режима кеширования:
method
— метод запроса;headers
— заголовки запроса (объект);body
— тело запроса: FormData, Blob, строка и т. д.;mode
— одно из: same-origin, no-cors, cors, указывает, в каком режиме кросс-доменности предполагается делать запрос;credentials
— одно из: omit, same-origin, include, указывает, пересылать ли cookies и заголовки авторизации вместе с запросом;cache
— одно из: default, no-store, reload, no-cache, force-cache, only-if-cached, указывает, как кешировать запрос.signal
свойство, в которое можно передать AbortController и с его помощью прервать запрос.
Отправим более сложный запрос. Например, на создание чата:
fetch('/api/v1/chats', {
method: 'POST',
body: JSON.stringify({
title: 'Мой чат',
}),
});
А вот как прервать этот запрос:
// Создаём экземпляр AbortController, который будет отвечать за прерывание запроса
const abortController = new AbortController();
const signal = abortController.signal;
fetch('/api/v1/chats', {
method: 'POST',
body: JSON.stringify({
title: 'Мой чат',
}),
// Указываем fetch, что signal может его прервать
signal
});
// Прерываем
abortController();
Также на данном этапе возникнет проблема при работе с cookies. Мы никак не указали, что, если при запросе есть cookies, браузеру необходимо их подставить. Это можно сделать с помощью свойств mode и credentials:
fetch('/api/v1/chats', {
method: 'POST',
mode: 'cors',
credentials: 'include',
body: JSON.stringify({
title: 'Мой чат',
}),
});
Реализация fetch
XHR очень мощный инструмент, но использовать только его, без обёрток, довольно сложно. Получится много копипаста, однотипных «ифов» и других проверок. Потому вам предстоит написать свою реализацию и воссоздать весь необходимый функционал.
Для начала напишем функцию, отправляющую просто запрос с помощью XHR:
function request(url: string, method: string): void {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function() {
console.log(xhr);
};
const handleError = err => {
console.log(err);
};
xhr.onabort = handleError;
xhr.onerror = handleError;
xhr.ontimeout = handleError;
xhr.send();
}
Теперь можно отправлять запросы вида request('url', 'GET') без постоянного копирования кода с XMLHttpRequest. Модифицируем функцию, чтобы она принимала более сложную конфигурацию:
type Options = {
method: string;
data?: any;
};
// Важно, чтобы у options было значение по умолчанию.
// Иначе, если не передать options, всё упадет с ошибками.
// А так как это поле обязательное — укажем его
function request(url: string, options: Options = { method: 'GET' }): void {
const {method, data} = options;
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.onload = function() {
console.log(xhr);
};
const handleError = err => {
console.log(err);
};
xhr.onabort = handleError;
xhr.onerror = handleError;
xhr.ontimeout = handleError;
if (method === 'GET' || !data) {
xhr.send();
} else {
xhr.send(JSON.stringify(data));
}
}
Обратите внимание на строку с GET
. Неудобно каждый раз проверять руками метод на равенство строке. А если поменяется запись с GET на get, придётся руками переписывать половину проекта. Небезопасно и неправильно. Чтобы избежать этого, применим enum:
enum METHOD {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
DELETE = 'DELETE'
};
type Options = {
method: METHOD;
data?: any;
};
function request(url: string, options: Options = { method: METHOD.GET }): void {
const {method, data} = options;
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.onload = function() {
console.log(xhr);
};
const handleError = err => {
console.error(err);
};
xhr.onabort = handleError;
xhr.onerror = handleError;
xhr.ontimeout = handleError;
if (method === METHOD.GET || !data) {
xhr.send();
} else {
xhr.send(JSON.stringify(data));
}
}
Отлично! Запросы уходят, но ответ пока попадает в консоль и не несёт никакой пользы. Применим промисификацию:
enum METHOD {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
DELETE = 'DELETE'
};
type Options = {
method: METHOD;
data?: any;
};
function request<TResponse>(url: string, options: Options = { method: METHOD.GET }): Promise<TResponse> {
const {method, data} = options;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function() {
resolve(xhr);
};
xhr.onabort = reject;
xhr.onerror = reject;
xhr.ontimeout = reject;
if (method === METHOD.GET || !data) {
xhr.send();
} else {
xhr.send(data);
}
});
}
Теперь можно работать с возвращаемым промисом (а если подложить в дженерик тип — возвращаемое значение будет типизировано):
request<{ id: string }>('https://chats', {
method: METHOD.POST,
data: JSON.stringify({
title: 'Мой чат',
}),
}).then(({ id }) => {
// Здесь id имеет тип string
});
Неудобно каждый раз описывать свойства запроса. Логика работы с XHR инкапсулирована, но копирование объектов конфигурации пока осталось. Хочется не писать лишних символов, а явно указывать — «сделай POST-запрос на вот этот урл, с вот такими данными»:
HTTP.post('https://chats', { title: 'Мой чат' });
Реализуем класс для работы с запросами:
enum METHOD {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
DELETE = 'DELETE'
};
type Options = {
method: METHOD;
data?: any;
};
// Тип Omit принимает два аргумента: первый — тип, второй — строка
// и удаляет из первого типа ключ, переданный вторым аргументом
type OptionsWithoutMethod = Omit<Options, 'method'>;
// Этот тип эквивалентен следующему:
// type OptionsWithoutMethod = { data?: any };
class HTTPTransport {
get(url: string, options: OptionsWithoutMethod = {}): Promise<XMLHttpRequest> {
return this.request(url, {...options, method: METHOD.GET});
};
request(url: string, options: Options = { method: METHOD.GET }): Promise<XMLHttpRequest> {
const {method, data} = options;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function() {
resolve(xhr);
};
xhr.onabort = reject;
xhr.onerror = reject;
xhr.ontimeout = reject;
if (method === METHOD.GET || !data) {
xhr.send();
} else {
xhr.send(data);
}
});
};
}
Теперь можно отправлять запросы вот так:
new HTTPTransport().get('https://chats');
Axios
Axios — модифицированный и более сложный аналог Fetch API. Кто-то предпочитает метод fetch
, кто-то axios. Некоторые заново пишут обёртки над XHR, чтобы не тащить библиотеку.
Интересное сравнение fetch и axios.
В проекте нельзя будет использовать ни Fetch API, ни axios. Вы сами реализуете в тренажёре аналог fetch
используя XMLHttpRequest
и будете использовать свою реализацию.