Как использовать Fetch API в Node.js, Deno и Bun

Источник: «How to use the Fetch API in Node.js, Deno, and Bun»
Узнайте, как использовать Fetch API - более простую и удобную альтернативу XMLHttpRequest, основанную на промисах, — с помощью Node.js, Deno и Bun.

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 сторонних разработчиков могут:

Вы можете использовать вызовы fetch() на стороне сервера для проксирования запросов на стороне клиента, чтобы избежать проблем с CORS и CSP. Тем не менее не забывайте быть добросовестным веб-гражданином и не бомбардируйте сервисы тысячами запросов, которые могут вывести их из строя!

Настраиваемые Fetch-запросы

В приведённом выше примере запрашиваются данные с URI https://example.com/data.json. Внутри JavaScript создаёт объект Request, представляющий все детали запроса, такие как метод, заголовки, тело и т. д.

fetch() принимает два аргумента:

Например:

const response = await fetch('https://example.com/data.json', {
method: 'GET',
credentials: 'omit',
redirect: 'error',
priority: 'high'
});

Объект options может задавать следующие свойства в Node.js или в коде на стороне клиента:

СвойствоЗначения
methodGET (по умолчанию), POST, PUT, PATCH, DELETE, или HEAD
headersстрока или объект Headers
bodyможет быть строкой, JSON, blob и т.д.
modesame-origin, no-cors, или cors
credentialsomit, same-origin, либо include (содержит) cookies и заголовки HTTP-аутентификации
redirectfollow, 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-запроса

Успешный вызов fetch() возвращает объект Response, содержащий информацию о состоянии и полученных данных. Свойствами являются:

СвойствоОписание
oktrue, если ответ был успешным
statusкод состояния HTTP, например 200 для успешного завершения
statusTextтекст статуса HTTP, например, OK для кода 200
urlURL
redirectedtrue, если запрос был переадресован
typeтип ответа: basic, cors, error, opaque или opaqueredirect
headersобъект Headers ответа
bodyReadableStream содержимого тела ответа (или null)
bodyUsedtrue, если тело ответа было прочитано

Все следующие методы объекта 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 возвращает массив объектов, в котором:

Итог

Если вы не используете устаревшую версию Node.js (17 или ниже), API Fetch доступен на JavaScript как на сервере, так и на клиенте. Он гибкий, простой в использовании и согласованный во всех средах выполнения. Сторонний модуль может потребоваться только в том случае, если вам нужна более продвинутая функциональность, такая как кэширование, повторные запросы или обработка файлов.

Дополнительные материалы

Предыдущая Статья

Текучая типографика для отзывчивого дизайна

Следующая Статья

Подробно: Знакомство с Random