Как использовать Fetch API в Node.js, Deno и Bun
- Fetch API vs XMLHttpRequest
- Базовый пример запроса fetch()
- Запрос на стороне клиента vs Запрос на стороне сервера
- Настраиваемые Fetch-запросы
- Обработка HTTP-заголовков
- Обработка промиса fetch()
- Анализ ответов Fetch-запроса
- Прерывание Fetch запросов
- Эффективные запросы
- Итог
Fetch API vs XMLHttpRequest
Получение данных с помощью HTTP-запроса — это фундаментальная задача веб-приложений. Вы, возможно, делали такие вызовы в браузере, но Fetch API поддерживается в Node.js, Deno и Bun.
В браузере вы можете запросить информацию с сервера, чтобы отобразить её без обновления всей страницы. Обычно это называется Ajax-запросом или одностраничным приложением (SPA). С 1999 по 2015 год XMLHttpRequest был единственным вариантом — и остаётся им, если вы хотите показать прогресс загрузки файла. XMLHttpRequest — это довольно громоздкий API, основанный на обратных вызовах, но он позволяет осуществлять тонкий контроль и, несмотря на название, может обрабатывать ответы в форматах, отличных от XML — таких, как текст, двоичный формат, JSON и HTML.
Браузеры реализовали Fetch API с 2015 года. Это более простая, лёгкая, более последовательная, основанная на промисах альтернатива XMLHttpRequest.
Возможно, вам также понадобится выполнять HTTP-запросы — как правило, для вызова API на других серверах. С момента своего первого релиза, как в Deno, так и в Bun было удобно копировать Fetch API браузера, чтобы аналогичный код мог выполняться как на клиенте, так и на сервере. Node.js требовал использования сторонних модулей, таких как node-fetch или axios, до февраля 2022 года, когда в версии 18 был добавлен стандартный Fetch API. Он всё ещё считается экспериментальным, но теперь в большинстве случаев вы можете использовать fetch()
везде с идентичным кодом.
Базовый пример запроса fetch()
В этом простом примере из URI извлекаются данные ответа:
const response = await fetch('https://example.com/data.json');
Вызов fetch()
возвращает промис, разрешающийся в объект Response, содержащий информацию о результате. Вы можете разобрать тело HTTP-ответа в JavaScript объект с помощью метода .json()
, основанного на промисе:
const data = await response.json();
// сделать что-то интересное с объектом данных
// ...
Запрос на стороне клиента vs Запрос на стороне сервера
API может быть идентичным на разных платформах, но браузеры накладывают ограничения при выполнении запросов fetch()
на стороне клиента:
Cross-origin resource sharing (CORS)
Клиентский JavaScript может взаимодействовать с конечными точками API только в пределах своего собственного домена. Скрипт, загруженный с https://domainA.com/js/main.js
, может вызывать любой сервис на https://domainA.com/
, например https://domainA.com/api/
или https://domainA.com/data/
.
Невозможно вызвать службу на https://domainB.com/
— если только сервер не разрешит доступ, установив заголовок HTTP Access-Control-Allow-Origin.
Content Security Policy (CSP)
Ваши веб-сайты/приложения могут устанавливать HTTP-заголовок Content-Security-Policy
или метатег для контроля разрешённых ресурсов на странице. Это может предотвратить случайное или злонамеренное внедрение скриптов, iframes, шрифтов, изображений, видео и так далее. Например, установка default-src 'self'
не позволяет функции fetch()
запрашивать данные за пределами собственного домена (также ограничиваются XMLHttpRequest, WebSocket, события, отправляемые сервером, и маячки).
Вызовы Fetch API на стороне сервера в Node.js, Deno и Bun имеют меньше ограничений, и вы можете запрашивать данные с любого сервера. Тем не менее API сторонних разработчиков могут:
- требовать аутентификации или авторизации с помощью ключей или OAuth
- иметь максимальные пороги запросов, например, не более одного запроса в минуту, или
- взимать плату за доступ
Вы можете использовать вызовы fetch()
на стороне сервера для проксирования запросов на стороне клиента, чтобы избежать проблем с CORS и CSP. Тем не менее не забывайте быть добросовестным веб-гражданином и не бомбардируйте сервисы тысячами запросов, которые могут вывести их из строя!
Настраиваемые Fetch-запросы
В приведённом выше примере запрашиваются данные с URI https://example.com/data.json
. Внутри JavaScript создаёт объект Request
, представляющий все детали запроса, такие как метод, заголовки, тело и т. д.
fetch()
принимает два аргумента:
resource
— строка или объект URL, и- необязательный параметр
options
с дополнительными настройками запроса
Например:
const response = await fetch('https://example.com/data.json', {
method: 'GET',
credentials: 'omit',
redirect: 'error',
priority: 'high'
});
Объект options
может задавать следующие свойства в Node.js или в коде на стороне клиента:
Свойство | Значения |
---|---|
method | GET (по умолчанию), POST , PUT , PATCH , DELETE , или HEAD |
headers | строка или объект Headers |
body | может быть строкой, JSON, blob и т.д. |
mode | same-origin , no-cors , или cors |
credentials | omit , same-origin , либо include (содержит) cookies и заголовки HTTP-аутентификации |
redirect | follow , error или manual (ручная) обработка редиректов |
referrer | ссылающийся URL |
integrity | хэш целостности подресурса |
signal | объект AbortSignal для отмены запроса |
Как вариант, вы можете создать объект Request
и передать его в fetch()
. Это может быть удобно, если вы можете заранее определить конечные точки API или хотите отправить серию одинаковых запросов:
const request = new Request('https://example.com/api/', {
method: 'POST',
body: '{"a": 1, "b": 2, "c": 3}',
credentials: 'omit'
});
console.log(`fetching ${ request.url }`);
const response = await fetch(request);
Обработка HTTP-заголовков
Вы можете манипулировать и анализировать HTTP-заголовки в запросах и ответах с помощью объекта Headers
. API будет вам знаком, если вы использовали JavaScript Maps
:
// устанавливаем начальные заголовки
const headers = new Headers({
'Content-Type': 'text/plain',
});
// добавляем заголовок
headers.append('Authorization', 'Basic abc123');
// добавление/изменение заголовка
headers.set('Content-Type', 'application/json');
// получение заголовка
const type = headers.get('Content-Type');
// содержит заголовок?
if (headers.has('Authorization')) {
// удаление заголовка
headers.delete('Authorization');
}
// перебор всех заголовков
headers.forEach((value, name) => {
console.log(`${ name }: ${ value }`);
});
// используем в fetch()
const response = await fetch('https://example.com/data.json', {
method: 'GET',
headers
});
// response.headers также возвращает объект Headers
response.headers.forEach((value, name) => {
console.log(`${ name }: ${ value }`);
});
Обработка промиса fetch()
Вы можете предположить, что промис fetch()
будет отклонён, если конечная точка вернёт 404 Not Found
или подобную ошибку сервера. Это не так! Промис разрешится, потому что вызов был успешным — даже если результат оказался не таким, как вы ожидали.
Промис fetch()
отклоняется только тогда, когда:
- Вы делаете некорректный запрос — например,
fetch('httttps://!invalid\URL/')
; - вы прервали запрос
fetch()
, или - произошла сетевая ошибка, например обрыв соединения
Анализ ответов Fetch-запроса
Успешный вызов fetch()
возвращает объект Response
, содержащий информацию о состоянии и полученных данных. Свойствами являются:
Свойство | Описание |
---|---|
ok | true , если ответ был успешным |
status | код состояния HTTP, например 200 для успешного завершения |
statusText | текст статуса HTTP, например, OK для кода 200 |
url | URL |
redirected | true , если запрос был переадресован |
type | тип ответа: basic , cors , error , opaque или opaqueredirect |
headers | объект Headers ответа |
body | ReadableStream содержимого тела ответа (или null ) |
bodyUsed | true , если тело ответа было прочитано |
Все следующие методы объекта Response
возвращают промис, поэтому следует использовать блоки await
или .then
:
Метод | Описание |
---|---|
text() | возвращает тело ответа в виде строки |
json() | разбирает тело ответа в объект JavaScript |
arrayBuffer() | возвращает тело ответа в виде ArrayBuffer |
blob() | возвращает тело ответа в виде Blob |
formData() | возвращает тело ответа как объект FormData , состоящий из пар ключ/значение |
clone() | клонирует ответ, обычно для того, чтобы можно было разобрать тело ответа разными способами |
// пример ответа
const response = await fetch('https://example.com/data.json');
// ответ вернул JSON?
if ( response.ok && response.headers.get('Content-Type') === 'application/json') {
// парсинг JSON
const obj = await response.json();
}
Прерывание Fetch запросов
Node.js не будет ждать тайм-аут при запросе fetch()
; он может выполняться вечно! Браузеры также могут ждать от одной до пяти минут. Прерывать fetch()
следует при нормальных условиях, когда вы ожидаете достаточно быстрого ответа.
В следующем примере используется объект AbortController
, передающий свойство signal
второму параметру fetch()
. Таймаут запускает метод .abort()
, если запрос не завершается в течение пяти секунд:
// создание AbortController для тайм-аута через 5 секунд
const
controller = new AbortController(),
signal = controller.signal,
timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://example.com/slowrequest/', { signal });
clearTimeout(timeout);
console.log( response.ok );
}
catch (err) {
// тайм-аут или ошибка сети
console.log(err);
}
Node.js, Deno, Bun и большинство браузеров, вышедших с середины 2022 года, также поддерживают AbortSignal
. Он предлагает более простой метод timeout()
, так что вам не нужно управлять собственными таймерами:
try {
// тайм-аут через 5 секунд
const response = await fetch('https://example.com/slowrequest/', {
signal: AbortSignal.timeout( 5000 ),
});
console.log( response.ok );
}
catch (err) {
// тайм-аут или ошибка сети
console.log(err);
}
Эффективные запросы
Как и в любой другой асинхронной операции, основанной на промисах, вам следует выполнять последовательные вызовы fetch()
только в тех случаях, когда параметры одного вызова зависит от результата предыдущего. Следующий код работает не так хорошо, как мог бы, потому что каждый вызов API должен ждать разрешения или отклонения предыдущего. Если каждый ответ занимает одну секунду, то в общей сложности на это уйдёт три секунды:
// неэффективно
const response1 = await fetch('https://example1.com/api/');
const response2 = await fetch('https://example2.com/api/');
const response3 = await fetch('https://example3.com/api/');
Метод Promise.allSettled()
запускает промисы одновременно и выполняет их, когда все они разрешены или отклонены. Этот код завершается со скоростью самого медленного ответа. Он будет выполняться в три раза быстрее:
const data = await Promise.allSettled(
[
'https://example1.com/api/',
'https://example2.com/api/',
'https://example3.com/api/'
].map(url => fetch( url ))
);
data
возвращает массив объектов, в котором:
- Каждое из них имеет свойство
status
—fullfilled
илиrejected
. - если разрешено, свойство
value
возвращает ответfetch()
- если отклонено, свойство
reason
возвращает ошибку
Итог
Если вы не используете устаревшую версию Node.js (17 или ниже), API Fetch доступен на JavaScript как на сервере, так и на клиенте. Он гибкий, простой в использовании и согласованный во всех средах выполнения. Сторонний модуль может потребоваться только в том случае, если вам нужна более продвинутая функциональность, такая как кэширование, повторные запросы или обработка файлов.