Керівництво по Fetch – зручний заміні XMLHttpRequest

12 хв. читання

Кожен раз, коли ми отримуємо або відправляємо дані за допомогою JavaScript, ми використовуємо Ajax. Ajax — це технологія, що дозволяє виконувати HTTP-запити без необхідності перезавантажувати сторінку.

Зауважте, що для прикладів ми будемо використовувати синтаксис ES6.

Декілька років тому найпростішим способом використовувати Ajax був метод jQuery.ajax:

$.ajax('some-url', {
  success: (data) => { /* обробка отриманих даних */ },
  error: (err) => { /* обробка помилки */}
});

Звісно, можна було й без цього, але тоді потрібно було писати XMLHttpRequest, що досить складно і неприємно.

Тепер браузери підтримують куди кращий інтерфейс — Fetch API, спосіб виконання Ajax-запитів без додаткових бібліотек. Сьогодні ми розлянемо як ним користуватися.

Підтримка Fetch

Так, спочатку слід переконатися, що код зможе виконатися у більшості користувачів.

Підтримка Fetch

Fetch підтримується досить добре. Більшість сучасних браузерів вже підтримують його нативно, тобто ви можете використовувати його в своїх проектах. А для тих браузерів, що його не підтримують, існує поліфіл.

Отримання даних з Fetch

Отримати дані за допомогою Fetch дуже просто. Вам лише потрібно передати URL ресурсу, який ви хочете отримати.

Наприклад, ми хочешо отримати список репозитаріїв Кріса на GitHub, нам потрібно звернутися до api.github.com/users/chriscoyier/repos:

fetch('https://web.archive.org/web/20230608175240/https://api.github.com/users/chriscoyier/repos');

Досить просто, чи не так? Але, що далі?

Fetch повертає проміс, тобто нам слід додати виклик .then:

fetch('https://web.archive.org/web/20230608175240/https://api.github.com/users/chriscoyier/repos')
  .then(response => {/* обробка */})

Більше про проміси тут.

Якщо ви до цього не працювали з Fetch, то будете здивовані кількістю даних, що повертаються. Якщо ви передасте результат виконання в console.log, то отримаєте наступну інформацію:

{
  body: ReadableStream
  bodyUsed: false
  headers: Headers
  ok : true
  redirected : false
  status : 200
  statusText : "OK"
  type : "cors"
  url : "http://some-website.com/some-url"
  __proto__ : Response
}

Як ви помітили, Fetch повернув дані, з яких можна визначити, що запит пройшов успішно (status дорівнює 200, а oktrue), але ми все ще не бачимо списку репозитаріїв.

Виявляється, потрібні нам дані сховані в body в вигляді потоку. І нас потрібно викликати підходящий метод щоб отримати необхідні дані.

Ми знаємо, що GitHub API повертає JSON, тому викликаємо метод response.json, щоб конвертувати дані.

Є методи для різних типів даних. Якщо ви працюєте з XML, ви використовуєте response.text, якщо з зображеннями — response.blob.

Всі ці методи повертають ще один проміс, тому щоб отримати потрібні дані, треба написати ще один .then.

fetch('https://web.archive.org/web/20230608175240/https://api.github.com/users/chriscoyier/repos')
  .then(response => response.json())
  .then(data => {
    // А тут вже буде список репозитаріїв
    console.log(data)
  });

Це все, що вам потрібно, щоб отримати дані з Fetch. Дуже просто! А тепер давайте спробуємо відправити дані.

Відправка даних з Fetch

Відправка даних теж нескладно. Для цього вам потрібно вказати три опції.

fetch('some-url', options);

Перша опція — це метод, який слід виконати (post, put або del). Якщо він не вказаний, Fetch виконає GET-запит.

Друга опція — це заголовки. Зазвичай ми відправляємо JSON, тому нам слід вказати Content-Type: application/json.

І третя опція — це тіло запиту, що міститиме JSON-дані. Отримати його можна за допомогою виклику JSON.stringify.

Наприклад, POST-запит на практиці виглядає так:

let content = {some: 'content'};

fetch('some-url', {
  method: 'post',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(content)
})
// .then()...

Ви могли помітити, що код виглядає трохи шаблонно, і, якщо ви робите декілька запитів, його можна винести в окрему функцію.

Обробка помилок

І хоча ми завжди сподіваємось на краще, іноді запити можуть виконуватися неправильно, або й взагалі не виконуватися. І для цього може бути багато причин, наприклад:

  1. Ви намагаєтесь звернутися до неіснуючого ресурсу
  2. Ви не авторизовані щоб отримати ці дані
  3. Ви помилились в аргументах
  4. Сервер повернув помилку
  5. Сервер не відповів вчасно
  6. Сервер не працює
  7. API змінилося
  8. І так далі...

Оброблювати помилки потрібно завжди. Давайте спробуємо отримати неіснуючий ресурс і подивимось на рекцію Fetch. Наприклад, зробимо одрук в нікнеймі:

fetch('https://web.archive.org/web/20230608175240/https://api.github.com/users/chrissycoyier/repos')

Для обробки помилок в промісах ми використовуємо метод catch.

В підсумку код обробки буде виглядати десь так:

fetch('https://web.archive.org/web/20230608175240/https://api.github.com/users/chrissycoyier/repos')
  .then(response => response.json())
  .then(data => console.log('data is', data))
  .catch(error => console.log('error is', error));

Після запиту ви побачите в консолі щось таке:

Керівництво по Fetch – зручний заміні XMLHttpRequest

Чому виконався другий .then? Хіба помилки в промісі не повинен відловлювати .catch?

Якщо ви виведете результат запису в консоль, то побачите:

{
  body: ReadableStream
  bodyUsed: true
  headers: Headers
  ok: false // Щось пішло не так
  redirected: false
  status: 404 // статус-код 404
  statusText: "Not Found" // Ресурс не знайдено
  type: "cors"
  url: "https://api.github.com/users/chrissycoyier/repos"
}

З цієї відповіді ви можете здогадатися, що Fetch не піклується про виконання вашого запиту. Він лише відправляє його і приймає відповідь. Викликати помилку потрібно вже вам.

Тобто нам потрібно трохи переписати перший then, щоб він перевіряв чи response.ok === true і перетворював тіло відповіді в JSON, а якщо ні — викидав помилку.

fetch('some-url')
  .then(response => {
    if (response.ok) {
      return response.json()
    } else {
      // Тут потрібно якось активувати .catch()
    }
  });

Коли ми розуміємо, що при виконанні запиту сталася помилка, ми можемо або викинути (throw) помилку, або виконати Promise.reject щоб викликати виконання catch.

else {
  throw new Error('something went wrong!')
}


else {
  return Promise.reject('something went wrong!')
}

Я надаю перевагу Promise.reject бо його простіше реалізувати. Викидати помилку теж можна, але це складніше реалізувати, а єдиною перевагою буде стек викликів (але не у випадку з Fetch).

Тепер наш код виглядає якось так:

fetch('https://web.archive.org/web/20230608175240/https://api.github.com/users/chrissycoyier/repos')
  .then(response => {
    if (response.ok) {
      return response.json()
    } else {
      return Promise.reject('something went wrong!')
    }
  })
  .then(data => console.log('data is', data))
  .catch(error => console.log('error is', error));

Керівництво по Fetch – зручний заміні XMLHttpRequest

Ми можемо дізнатися коли щось пішло не так. Але ми не знаємо, що саме. Давайте це виправимо.

Розглянемо відповідь сервера знову і подивимось, що тут вказує на помилку:

{
  body: ReadableStream
  bodyUsed: true
  headers: Headers
  ok: false // Щось пішло не так
  redirected: false
  status: 404 // статус-код 404
  statusText: "Not Found" // Ресурс не знайдено
  type: "cors"
  url: "https://api.github.com/users/chrissycoyier/repos"
}

Окей, в даному випадку ресурсу не існує, ми це знаємо. Передаватимемо код 404 або статус Not Found щоб повідомити про помилку. А повідомити про неї можна, передавши JS-об'єкт в Promise.resolve:

fetch('some-url')
  .then(response => {
    if (response.ok) {
      return response.json()
    } else {
      return Promise.reject({
        status: response.status,
        statusText: response.statusText
      })
    }
  })
  .catch(error => {
    if (error.status === 404) {
      // обробка помилки 404
    }
  })

Такої системи достатньо якщо виникла помилка, суть якої зрозуміло з назви:

  • 401: Unauthorized
  • 404: Not found
  • 408: Connection timeout
  • ...

Але що відрізняє поганий запит від нормального? Stripe, наприклад, повертає 400, якщо відсутній обов'язковий параметр.

Керівництво по Fetch – зручний заміні XMLHttpRequest

Недостатньо просто передати номер помилки в .catch, потрібно більше інформації. В такому випадку було б непогано, якби сервер додатково повертав обєкт, що описує помилку. Якщо ви використовуєте Node та Express, це може виглядати так:

res.status(400).send({
  err: 'no first name'
})

Тепер ми не можемо викликати reject в першому then, адже інформація про помилку міститься в відповіді сервера.

В такому випадку ми можемо використовувати проміс з двома викликами then. Спочатку ми читаємо response.json, а потім вирішуємо що з ним робити.

fetch('some-error')
  .then(handleResponse)

function handleResponse(response) {
  return response.json()
    .then(json => {
      if (response.ok) {
        return json
      } else {
        return Promise.reject(json)
      }
    })
}

Якщо ви хочете повертати статус код разом з JSON, ви можете скористатися методом Object.assign():

let error = Object.assign({}, json, {
  status: response.status,
  statusText: response.statusText
})
return Promise.reject(error)

З новою функцією handleResponse ви можете писати ваш код ось так: (дані передаються в then автоматично catch)

fetch('some-url')
  .then(handleResponse)
  .then(data => console.log(data))
  .catch(error => console.log(error))

Обробка

Зараз наш код може обробляти лише JSON-дані. Хоча це покриває 90% всіх потреб, цей туторіал буде неповним, якщо я не розкажу вам як отримати XML чи бінарні дані.

Якщо ви спробуєте отримати XML за допомогою коду вище, то отримаєте ось таку помилку:

Керівництво по Fetch – зручний заміні XMLHttpRequest

Це все через те, що JS намагається розпарсити XML як JSON. Тепер нам потрібно замість response.json вкористовувати response.text:

.then(response => {
  let contentType = response.headers.get('content-type')

  if (contentType.includes('application/json')) {
    return response.json()
    // ...
  }

  else if (contentType.includes('text/html')) {
    return response.text()
    // ...
  }

  else {
    // Обробка других типів даних
  }
});

І ось так наш код буде виглядати в кінці:

fetch('some-url')
  .then(handleResponse)
  .then(data => console.log(data))
  .then(error => console.log(error))

function handleResponse (response) {
  let contentType = response.headers.get('content-type')
  if (contentType.includes('application/json')) {
    return handleJSONResponse(response)
  } else if (contentType.includes('text/html')) {
    return handleTextResponse(response)
  } else {
    throw new Error(`Sorry, content-type ${contentType} not supported`)
  }
}

function handleJSONResponse (response) {
  return response.json()
    .then(json => {
      if (response.ok) {
        return json
      } else {
        return Promise.reject(Object.assign({}, json, {
          status: response.status,
          statusText: response.statusText
        }))
      }
    })
}
function handleTextResponse (response) {
  return response.text()
    .then(text => {
      if (response.ok) {
        return text
      } else {
        return Promise.reject({
          status: response.status,
          statusText: response.statusText,
          err: text
        })
      }
    })
}

Так, щоб зручно використовувати Fetch, вам спершу потрібно написати трохи сніппетів. Одного разу мені це набридло і я написав бібліотеку, що робить те, що я описав в статті.

zlFetch

zlFetch — бібліотека, що абстрагує вас від функції handleResponse.

zlFetch('some-url', options)
  .then(data => console.log(data))
  .catch(error => console.log(error));

Щоб використовувати zlFetch вам потрібно його встановити:

npm install zl-fetch --save

І імпортувати до вашого коду. Зверніть увагу, що поліфіли потрібно додавати перед zlFetch.

// Поліфіли (якщо потрібно)
require('isomorphic-fetch')

// ES6 імпорт
import zlFetch from 'zl-fetch';

// CommonJS імпорт
const zlFetch = require('zl-fetch');

Також zlFetch допомагає вам надсилати JSON: тепер вам не потрібно вказувати заголовки та серіалізувати дані, zlFetch зробить це за вас. Наступні шматки коду роблять те ж саме:

let content = {some: 'content'}

fetch('some-url', {
  method: 'post',
  headers: {'Content-Type': 'application/json'}
  body: JSON.stringify(content)
});

zlFetch('some-url', {
  method: 'post',
  body: content
});

zlFetch також допомагає з використанням JWT.

Загальною практикою при використанні JWT є вказання заголовка Authorization зі значенням Bearer <ваш токен>. zlFetch трошки автоматизує це:

let token = 'someToken'
zlFetch('some-url', {
  headers: {
    Authorization: `Bearer ${token}`
  }
});

// Або просто так:
zlFetch('some-url', {token});
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.7K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація