fetch()

Модно и современно отправляем запросы на сервер.

Время чтения: меньше 5 мин

Кратко

Скопировано

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

Как понять

Скопировано

Браузер предоставляет глобальный API для работы с запросами и ответами HTTP. Раньше для подобной работы использовался XMLHttpRequest, однако fetch() более гибкая и мощная альтернатива. Он понятнее и проще в использовании из-за того, что использует Promise.

Как пишется

Скопировано

Функция fetch() принимает два параметра:

  • url — адрес, по которому нужно сделать запрос;
  • options (необязательный) — объект конфигурации, в котором можно настроить метод и тело запроса, заголовки и многое другое.

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

        
          
          fetch('http://jsonplaceholder.typicode.com/posts')
          fetch('http://jsonplaceholder.typicode.com/posts')

        
        
          
        
      

Результатом вызова fetch() будет Promise, в котором содержится специальный объект ответа Response. У этого объекта есть два важных для нас поля:

  • ok — принимает состояние true или false и сообщает об успешности запроса;
  • json — метод, вызов которого, возвращает результат запроса в виде JSON.

В следующем примере используем .then() — обработчик результата, полученного от асинхронной операции. Обработчик дождётся ответа от сервера, принимает ответ, и, в данном случае, неявно вернёт ответ, обработанный методом .json().

В примере функция then вернёт другой промис (их можно объединять). Когда отрезолвится промис (r.json()), который вернула функция then, будет вызван следующий колбэк в цепочке.

        
          
          fetch('http://jsonplaceholder.typicode.com/posts')  .then((response) => response.json()  // Получим ответ в виде массива из объектов:  // [{...}, {...}, {...}, ...])
          fetch('http://jsonplaceholder.typicode.com/posts')
  .then((response) => response.json()
  // Получим ответ в виде массива из объектов:
  // [{...}, {...}, {...}, ...]
)

        
        
          
        
      

С помощью второго аргумента options можно передать настройки запроса. Например, можно изменить метод и добавить тело запроса, если мы хотим не получать, а отправлять данные. Также в запрос можно добавить заголовки в виде объекта или специального класса Headers.

        
          
          const newPost = {  title: 'foo',  body: 'bar',  userId: 1,}fetch('https://jsonplaceholder.typicode.com/posts', {  method: 'POST', // Здесь так же могут быть GET, PUT, DELETE  // Тело запроса в JSON-формате  body: JSON.stringify(newPost),  headers: {    // Добавляем необходимые заголовки    'Content-type': 'application/json; charset=UTF-8',  },})  .then((response) => response.json())  .then((data) => {    console.log(data)    // {title: "foo", body: "bar", userId: 1, id: 101}  })
          const newPost = {
  title: 'foo',
  body: 'bar',
  userId: 1,
}

fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST', // Здесь так же могут быть GET, PUT, DELETE
  // Тело запроса в JSON-формате
  body: JSON.stringify(newPost),
  headers: {
    // Добавляем необходимые заголовки
    'Content-type': 'application/json; charset=UTF-8',
  },
})
  .then((response) => response.json())
  .then((data) => {
    console.log(data)
    // {title: "foo", body: "bar", userId: 1, id: 101}
  }
)

        
        
          
        
      

Cookies

Скопировано

По умолчанию fetch() запросы не включают в себя cookies, и поэтому авторизованные запросы на сервере могут не пройти. Для этого необходимо добавить в настройку поле credentials:

        
          
          fetch('https://somesite.com/admin', {  method: 'GET',  // Или same-origin, если можно делать такие запросы  // только в пределах этого домена  credentials: 'include',})
          fetch('https://somesite.com/admin', {
  method: 'GET',
  // Или same-origin, если можно делать такие запросы
  // только в пределах этого домена
  credentials: 'include',
})

        
        
          
        
      

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

Скопировано

Любой ответ на запрос через fetch(), например, HTTP-код 400, 404 или 500, переводит Promise в состояние fulfilled. Промис перейдёт в состояние rejected только если запрос не случился из-за сбоя сети или что-то помешало выполнению fetch().

        
          
          // Запрос вернёт ошибку «404 Not Found»fetch('https://jsonplaceholder.typicode.com/there-is-no-such-route').catch(  () => {    console.log('Error occurred!')  })// Никогда не выполнится
          // Запрос вернёт ошибку «404 Not Found»
fetch('https://jsonplaceholder.typicode.com/there-is-no-such-route').catch(
  () => {
    console.log('Error occurred!')
  }
)
// Никогда не выполнится

        
        
          
        
      

Чтобы обработать ошибку запроса, необходимо обращать внимание на поле ok в объекте ответа Response. В случае ошибки запроса оно будет равно false.

        
          
          fetch('https://jsonplaceholder.typicode.com/there-is-no-such-route')  .then((response) => {    // Проверяем успешность запроса и выкидываем ошибку    if (!response.ok) {      throw new Error('Error occurred!')    }    return response.json()  })  // Теперь попадём сюда, так как выбросили ошибку  .catch((err) => {    console.log(err)  })// Error: Error occurred!
          fetch('https://jsonplaceholder.typicode.com/there-is-no-such-route')
  .then((response) => {
    // Проверяем успешность запроса и выкидываем ошибку
    if (!response.ok) {
      throw new Error('Error occurred!')
    }

    return response.json()
  })
  // Теперь попадём сюда, так как выбросили ошибку
  .catch((err) => {
    console.log(err)
  }
)
// Error: Error occurred!

        
        
          
        
      

На практике

Скопировано

Егор Огарков советует

Скопировано

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

Скопировано

Иногда может возникнуть необходимость прервать запрос, например, когда авторизация пользователя просрочена или пользователь самостоятельно хочет отменить запрос (отменил скачивание файла).

        
          
          const controller = new AbortController()function fetchData() {  return fetch('http://jsonplaceholder.typicode.com/posts', {    signal: controller.signal,  })    .then((response) => response.json())    .catch((e) => {      console.log(e)    }  )}fetchData()// Если запрос ещё не выполнился, то он будет прерван.// Прерванный fetch вернёт Promise с ошибкойcontroller.abort()
          const controller = new AbortController()

function fetchData() {
  return fetch('http://jsonplaceholder.typicode.com/posts', {
    signal: controller.signal,
  })
    .then((response) => response.json())
    .catch((e) => {
      console.log(e)
    }
  )
}

fetchData()

// Если запрос ещё не выполнился, то он будет прерван.
// Прерванный fetch вернёт Promise с ошибкой
controller.abort()

        
        
          
        
      

Запрос не выполнится, в консоли будет ошибка The user aborted a request. Если заглянуть в инструменты разработчика, то там можно увидеть отменённый статус у запроса.

Пример прерванного запроса

Загрузка файла на сервер

Скопировано

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

        
          
          <form id="form">  <input type="file" id="avatar">  <button type="submit">Загрузить</button></form>
          <form id="form">
  <input type="file" id="avatar">
  <button type="submit">Загрузить</button>
</form>

        
        
          
        
      
        
          
          // Находим элемент с файломconst fileInput = document.getElementById('avatar')const form = document.getElementById('form')function handleSubmit(event) {  event.preventDefault()  const formData = new FormData()  // Добавляем файлы из инпута к данным  for (let i = 0; i < fileInput.files.length; i++) {    const file = fileInput.files[i]    formData.append('avatar', file, file.name)  }  // Отправляем файлы на сервер  fetch('https://backend.com/api/upload', {    method: "POST",    body: formData,  })}form.addEventListener('submit', handleSubmit)
          // Находим элемент с файлом
const fileInput = document.getElementById('avatar')
const form = document.getElementById('form')

function handleSubmit(event) {
  event.preventDefault()

  const formData = new FormData()

  // Добавляем файлы из инпута к данным
  for (let i = 0; i < fileInput.files.length; i++) {
    const file = fileInput.files[i]
    formData.append('avatar', file, file.name)
  }

  // Отправляем файлы на сервер
  fetch('https://backend.com/api/upload', {
    method: "POST",
    body: formData,
  })
}

form.addEventListener('submit', handleSubmit)

        
        
          
        
      

Скачивание данных с результатом прогресса

Скопировано

Чтобы получать текущий прогресс скачивания файла или любых других данных, используйте свойство body объекта Response, который возвращается в Promise после вызова fetch(). Поле body является «потоком для чтения» (Readable Stream). Это специальный объект, который даёт возможность получать информацию по частям, по мере её поступления на клиент.

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

        
          
          fetch('https://i.imgur.com/C5QXZ7u.mp4').then(async (response) => {  let received = 0  // Получаем поток в переменную  const reader = response.body.getReader()  // Считываем общую длину данных  const contentLength = parseInt(response.headers.get('Content-Length'), 10)  while (true) {    // После вызова read() возвращается объект, в котором    // done — boolean-значение о том, закончилась ли информация,    // value — массив байт, которые пришли в этот раз    const { done, value } = await reader.read()    if (done) {      console.log('Получено 100%')      break    }    received += Math.ceil(contentLength / value.length)    console.log(`Получено ${received}%`)  }})
          fetch('https://i.imgur.com/C5QXZ7u.mp4').then(async (response) => {
  let received = 0

  // Получаем поток в переменную
  const reader = response.body.getReader()

  // Считываем общую длину данных
  const contentLength = parseInt(response.headers.get('Content-Length'), 10)

  while (true) {
    // После вызова read() возвращается объект, в котором
    // done — boolean-значение о том, закончилась ли информация,
    // value — массив байт, которые пришли в этот раз
    const { done, value } = await reader.read()

    if (done) {
      console.log('Получено 100%')
      break
    }

    received += Math.ceil(contentLength / value.length)

    console.log(`Получено ${received}%`)
  }
})