[Перевод] Как использовать Fetch API в Node.js, Deno и Bun
Ключевые моменты
Fetch API в современных средах JavaScript: в этой статье рассмотрим, как использовать Fetch API, современную и более простую альтернативу XMLHttpRequest, в различных средах JavaScript — таких как Node.js, Deno и Bun. Уделим особое внимание его структуре, основанной на промисах, а также простоте использования.
Различия в использовании fetch на стороне клиента и на стороне сервера: хотя Fetch API предоставляет единый интерфейс, важно учитывать различия в ограничениях, таких как CORS и CSP на стороне клиента и потенциальные ограничения API сторонних разработчиков на стороне сервера.
Важность эффективных стратегий fetch запросов: статья подчёркивает использование
Promise.allSettled
для параллельных запросов и управление таймаутами с AbortController для оптимизации производительности и обработки ошибок в веб-приложениях.
Fetch API против XMLHttpRequest
Получение данных с помощью HTTP-запроса — это фундаментальное действие веб-приложений. Возможно, вы делали такие вызовы в браузере, но Fetch API поддерживается в Node.js, Deno и Bun.
В браузере можно запросить информацию с сервера, чтобы отобразить её без обновления всего экрана. Обычно это называется Ajax-запросом или одностраничным приложением (single page application, SPA). С 1999 по 2015 год XMLHttpRequest был единственным вариантом — и остаётся им в том случае, если вам нужно показать прогресс загрузки файла. XMLHttpRequest — это довольно громоздкий API, основанный на обратных вызовах, но он позволяет осуществлять тщательный контроль. Также, несмотря на своё название, он может обрабатывать ответы в форматах, отличных от XML — таких как текст, двоичный формат, JSON и HTML.
Браузеры используют Fetch API с 2015 года. Это более простая, лёгкая, последовательная, основанная на промисах альтернатива XMLHttpRequest.
Код на стороне сервера также может отправлять HTTP-запросы для вызова API на других серверах. С момента своего первого релиза, как в Deno, так и в Bun runtimes было удобно копировать 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();
// do something exciting with the data object
// ...
Fetch на стороне клиента и на стороне сервера
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, события, отправляемые сервером, и маячки).
Вызовы API Fetch на стороне сервера в Node.js, Deno и Bun имеют меньше ограничений, данные можно запрашивать с любого сервера. При этом API сторонних разработчиков могут:
требовать аутентификации или авторизации с помощью ключей или OAuth;
иметь максимальные пороги запросов, например не более одного вызова в минуту, или
взимать коммерческую плату за доступ.
Чтобы избежать проблем с CORS и CSP, можно использовать вызовыfetch()
на стороне сервера для проксирования запросов на стороне клиента. При этом не забывайте оставаться добросовестным веб-гражданином и не бомбардируйте сервисы тысячами запросов, которые могут вывести их из строя.
Пользовательские 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 или в коде на стороне клиента:
свойство | значения |
|
|
| строка или объект Headers |
| может быть string, JSON, blob, и так далее. |
|
|
|
|
|
|
| ссылающийся URL |
| хэш целостности подресурса |
| объект 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:
// set inital headers
const headers = new Headers({
'Content-Type': 'text/plain',
});
// add header
headers.append('Authorization', 'Basic abc123');
// add/change header
headers.set('Content-Type', 'application/json');
// get a header
const type = headers.get('Content-Type');
// has a header?
if (headers.has('Authorization')) {
// delete a header
headers.delete('Authorization');
}
// loop through all headers
headers.forEach((value, name) => {
console.log(`${ name }: ${ value }`);
});
// use in fetch()
const response = await fetch('https://example.com/data.json', {
method: 'GET',
headers
});
// response.headers also returns a Headers object
response.headers.forEach((value, name) => {
console.log(`${ name }: ${ value }`);
});
Функции исполнения и отклонения
Можно было бы предположить, что промис fetch()
будет отклонён, если конечная точка вернет 404 Not Found
или подобную ошибку сервера. Это не так! Промис выполнится, потому что вызов был успешным — даже если результат оказался не таким, как вы ожидали.
Промис fetch()
отклоняется только в случае, если:
вы делаете некорректный запрос — например,
fetch('httttps://!invalid\URL/');
вы прерываете запрос
fetch()
, илипроизошла сетевая ошибка, например, разрыв соединения.
Анализ ответов на запрос fetch
Успешные вызовы fetch()
возвращают объект Response, который содержит информацию о состоянии и возвращённых данных. Свойствами объекта являются:
свойство | описание |
|
|
| код состояния HTTP, например 200 для успеха |
| текст статуса HTTP, например, |
| URL |
|
|
| тип ответа: |
| ответ объекта Headers |
| ReadableStream содержимого тела (или null) |
|
|
Все следующие методы объекта Response возвращают промис, поэтому следует использовать блоки await
или .then
:
метод | описание |
| возвращает тело в виде строки |
| преобразовывает тело в объект JavaScript |
| возвращает тело в виде ArrayBuffer |
| возвращает тело в виде Blob |
| возвращает тело как объект FormData, состоящий из пар ключ/значение |
| клонирует ответ — обычно для того, чтобы можно было преобразовать тело ответа разными способами |
// example response
const response = await fetch('https://example.com/data.json');
// response returned JSON?
if ( response.ok && response.headers.get('Content-Type') === 'application/json') {
// parse JSON
const obj = await response.json();
}
Прерывание fetch запросов
Node.js не будет прерывать запрос fetch()
; он может выполняться вечно. Браузеры также могут ждать от одной до пяти минут. Прерывать fetch()
следует при нормальных обстоятельствах, когда вы ожидаете достаточно быстрого ответа.
В следующем примере используется объект AbortController, который передаёт свойство signal
второму параметру fetch()
. Если fetch не завершится в течение пяти секунд, запустится метод .abort()
:
// create AbortController to timeout after 5 seconds
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) {
// timeout or network error
console.log(err);
}
AbortSignal поддерживается Node.js, Deno, Bun и большинством браузеров, выпущенных с середины 2022 года. Он предлагает более простой метод timeout (), так что вам не придётся управлять собственными таймерами:
try {
// timeout after 5 seconds
const response = await fetch('https://example.com/slowrequest/', {
signal: AbortSignal.timeout( 5000 ),
});
console.log( response.ok );
}
catch (err) {
// timeout or network error
console.log(err);
}
Эффективные вызовы fetch
Как и в любой асинхронной операции, основанной на промисах, последовательные вызовыfetch()
следует выполнять только в тех случаях, когда ввод вызова зависит от вывода предыдущего. Код ниже работает не так хорошо, как мог бы, потому что каждый вызов API должен ждать разрешения или отклонения предыдущего. Если каждый ответ занимает одну секунду, то в общей сложности на это уйдёт три секунды:
// inefficent
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 или ниже), Fetch API доступен на JavaScript как на сервере, так и на клиенте. Он гибкий, простой в использовании и согласованный во всех средах выполнения. Сторонний модуль может потребоваться только в том случае, если вам нужна более продвинутая функциональность, такая как кэширование, повторные запросы или обработка файлов.
Если вы хотите подтянуть свои знания инструментов веб-разработки, приходите на онлайн-курсы в OTUS от экспертов-практиков.