Оригинал: How to Use Fetch with async/await, автор Dmitri Pavlutin

Fetch API — это инструмент для выполнения сетевых запросов в веб-приложениях. При базовом использовании fetch() — довольно простой метод, но у него есть много нюансов. Например, мы не можем прервать fetch-запрос.

В этой статье рассмотрим сценарии использования fetch() вместе с другой возможностью языка — синтаксисом async/await. Разберемся, как получать данные, обрабатывать ошибки и отменять запросы.

Введение в fetch()

Fetch API позволяет делать HTTP-запросы (GET, POST и т. д.) и обмениваться данными с сервером. Это более удобный аналог XHR.

Чтобы выполнить запрос, просто вызовите функцию fetch():

const response = await fetch(resource[, options]);
  • Первый параметр resource — это URL-адрес запроса и объект Request.
  • Второй (необязательный) параметр options — это конфигурация запроса. Можно настроить method, header, body, credentials и другие опции.

Функция fetch выполняет запрос и возвращает промис, который будет ждать, когда запрос завершится. После этого промис выполняется (resolve) с объектом Response (ответ сервера). Если во время запроса произошла какая-то ошибка, промис переходит в состояние rejected.

Синтаксис async/await прекрасно сочетается с fetch() и помогает упростить работу с промисами. Давайте для примера сделаем запрос списка фильмов:

async function fetchMovies() {
  const response = await fetch('/movies');
  // ждем выполнения запроса
  console.log(response);
}

Функция fetchMovies асинхронная, используем для ее создания ключевое слово async. Внутри она использует await, чтобы дождаться выполнения асинхронной операции fetch.

Внутри функции выполняется запрос на урл /movies. Когда он успешно завершается, мы получаем объект response с ответом сервера. Дальше в статье мы разберемся, как извлечь данные из этого объекта.

Получение JSON

Из объекта response, который возвращается из await fetch() можно извлечь данные в нескольких разных форматах. Чаще всего используется JSON:

async function fetchMoviesJSON() {
  const response = await fetch('/movies');
  const movies = await response.json();
  return movies;
}
fetchMoviesJSON().then(movies => {
  movies; // полученный список фильмов
});

Итак, чтобы извлечь полученные данные в виде JSON, нужно использовать метод response.json(). Этот метод возвращает промис, так что придется снова воспользоваться синтаксисом await, чтобы дождаться его выполнения: await response.json().

Кроме того, у объекта Response есть еще несколько полезных методов (все методы возвращают промисы):

  • response.json() возвращает промис, который резолвится в JSON-объект;
  • response.text() возвращает промис, который резолвится в обычный текст;
  • response.formData() возвращает промис, который резолвится в объект FormData;
  • response.blob() возвращает промис, который резолвится в Blob (файлоподобный объект с необработанными данными);
  • response.arrayBuffer()() возвращает промис, который резолвится в ArrayBuffer (необработанные двоичные данные).

Обработка ошибок

Для разработчиков, которые только начинают работать с fetch, может быть непривычным то, что этот метод не выбрасывает исключение, если сервер возвращает «плохой» HTTP-статус (клиентские 400-499 или серверные 500-599 ошибки).

Попробуем для примера обратиться к несуществующей странице /oops. Этот запрос, как и ожидается, завершается со статусом 404.

async function fetchMovies404() {
  const response = await fetch('/oops');
  
  response.ok;     // => false
  response.status; // => 404
  const text = await response.text();
  return text;
}
fetchMovies404().then(text => {
  text; // => 'Page not found'
});

Из объекта response мы можем узнать, что запрос не удался, однако метод fetch() не выбрасывает ошибку, а считает этот запрос завершенным.

Промис, возвращаемый функцией fetch, отклоняется только в том случае, если запрос не может быть выполнен или ответ не может быть получен, например, из-за проблем с сетью (нет подключения, хост не найден, сервер не отвечает).

Но к счастью, у нас есть поле response.ok, с помощью которого мы можем отловить плохие статусы. Оно принимает значение true только если статус ответа 200-299.

Если вы хотите получать ошибку для всех неудачных запросов, просто выбрасывайте ее вручную:

async function fetchMoviesBadStatus() {
  const response = await fetch('/oops');
  if (!response.ok) {
    const message = `An error has occured: ${response.status}`;
    throw new Error(message);
  }
  const movies = await response.json();
  return movies;
}
fetchMoviesBadStatus().catch(error => {
  error.message; // 'An error has occurred: 404'
});

Отмена fetch-запроса

К сожалению, Fetch API не предоставляет нам никакой возможности отменить запущенный запрос. Но это можно сделать с помощью AbortController.

Чтобы объединить эти инструменты, нужно сделать 3 действия:

// Шаг 1. Создать экземпляр AbortController до начала запроса
const controller = new AbortController();

// Step 2: Передать в параметры запроса fetch() controller.signal
fetch(..., { signal: controller.signal });

// Step 3: Отменить запрос методом controller.abort при необходимости
controller.abort();

Давайте для примера создадим маленькое приложение с двумя кнопками, одна из которых будет запускать fetch-запрос, а вторая прерывать его.

let controller = null;

// Обрабатываем клики по первой кнопке
fetchMoviesButton.addEventListener('click', async () => {
  controller = new AbortController();
  try {
    const response = await fetch('/movies', { 
      signal: controller.signal 
    });
  } catch (error) {
    console.log('Fetch error: ', error);
  }
  controller = null;
});

// Обрабатываем клики по второй кнопке
cancelFetchButton.addEventListener('click', () => {
  if (controller) {
    controller.abort();
  }
});

Демо:

Кликните по кнопке Fetch movies, чтобы запустить запрос, а затем по кнопке Cancel fetch, чтобы отменить его. При этом возникнет ошибка, которую поймает блок .catch().

Экземпляр AbortController одноразовый, его нельзя переиспользовать для нескольких запросов. Поэтому для каждого вызова fetch нужно создавать новый инстанс.

Параллельные fetch запросы

Чтобы выполнять fetch-запросы параллельно, можно воспользоваться методом Promise.all().

Например, запустим сразу два запроса — для получения фильмов и для получения категорий фильмов:

async function fetchMoviesAndCategories() {
  const [moviesResponse, categoriesResponse] = await Promise.all([
    fetch('/movies'),
    fetch('/categories')
  ]);
  const movies = await moviesResponse.json();
  const categories = await categoriesResponse.json();
  return [movies, categories];
}
fetchMoviesAndCategories().then(([movies, categories]) => {
  movies;     // список фильмов
  categories; // список категорий
}).catch(error => {
  // один из запросов завершился с ошибкой
});

Фрагмент кода await Promise.all([]) запускает запросы параллельно и ожидает, когда все они перейдут в состояние resolved.

Если один из запросов завершится с ошибкой, то Promise.all тоже выбросит ошибку.

Если же вы хотите, чтобы выполнились все запросы, даже если несколько из них упали, используйте метод Promise.allSettled().

Заключение

Вызов функции fetch() запускает запрос к серверу и возвращает промис. Когда запрос успешно завершается, промис переходит в состояние resolved и возвращает объект ответа (response), из которого можно извлечь данные в одном из доступных форматов (JSON, необработанный текст или Blob).

Так как fetch возвращает промис, мы можем использовать синтаксис async/await, чтобы упростить код: response = await fetch().

В статье мы разобрали, как использовать эту комбинацию для получения данных, обработки ошибок, отмены запросов и выполнения параллельных запросов.

0 комментариев

Оставить комментарий

*Доступные HTML-теги: a, abbr, blockquote, code, pre, del, i, em, strong, b, strike
*Не будет опубликован