[Перевод] Как использовать Fetch API в Node.js, Deno и Bun

726640bfd3cfcdf61d2861636b20926a.png

Ключевые моменты

  • 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 или в коде на стороне клиента:

свойство

значения

method

GET (по умолчанию), POST, PUT, PATCH, DELETE, и HEAD

headers

строка или объект Headers

body

может быть string, JSON, blob, и так далее.

mode

same-origin, no-cors, и cors

credentials

omit, same-origin или include куки и заголовки HTTP-аутентификации

redirect

follow, error или ручная (manua) обработка редиректов

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:

// 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, который содержит информацию о состоянии и возвращённых данных. Свойствами объекта являются:

свойство

описание

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()

клонирует ответ — обычно для того, чтобы можно было преобразовать тело ответа разными способами

// 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 от экспертов-практиков.

© Habrahabr.ru