2 листопада 2024 р.

Fetch

JavaScript може відправляти мережеві запити на сервер та підвантажувати нову інформацію за потребою.

Наприклад, можна використовувати мережевий запит, щоб:

  • Відправляти замовлення,
  • Завантажити інформацію про користувача,
  • Отримати останні оновлення з сервера,
  • …і т.д.

…І все це без перезавантаження сторінки!

Є загальний термін “AJAX” (абревіатура від Asynchronous JavaScript And XML) для мережевих запитів від JavaScript коду. Але формат XML використовувати не обов’язково: цей термін застарілий, тому це слово (XML) тут. Можливо, ви вже його десь чули.

Є кілька способів надіслати мережевий запит і отримати інформацію з сервера.

Метод fetch() – сучасний та дуже потужний, тому почнемо з нього. Він не підтримується старими (можна використовувати поліфіл), але підтримується всіма сучасними браузерами.

Базовий синтаксис:

let promise = fetch(url, [options])
  • url – URL для відправлення запиту.
  • options – додаткові параметри: метод, заголовки і т.д.

Без options, це просто GET запит, який завантажує зміст за адресою url.

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

Процес отримання запиту зазвичай відбувається у два етапи.

По-перше, promise завершиться із об’єктом вбудованого класу Response у якості результату, одразу коли сервер надішле заголовки відповіді.

На цьому етапі можна перевірити статус HTTP-запиту, та визначити, чи виконався він успішно, а також переглянути заголовки, але поки що без тіла запиту.

Проміс закінчується помилкою, якщо fetch не зміг виконати HTTP-запит, наприклад, через помилку мережі або, якщо такого сайту не існує. Ненормальні HTTP-статуси, як 404 та 500, не викликатимуть помилку.

Ми можемо побачити HTTP-статус у властивостях відповіді:

  • status – код статуса HTTP-запиту, наприклад, 200.
  • ok – логічне значення, котре буде true, якщо код HTTP-статосу в діапазоні 200-299.

Наприклад:

let response = await fetch(url);

if (response.ok) { // якщо HTTP-статус у діапазоні 200-299
  // отримання тіла запиту (див. про цей метод нижче)
  let json = await response.json();
} else {
  alert("HTTP-Error: " + response.status);
}

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

Response надає декілька методів, які повертають проміс, для доступу до тіла запиту в різних форматах:

  • response.text() – читає відповід та повертає, як звичайний текст,
  • response.json() – декодує відповідь у форматі JSON,
  • response.formData() – повертає відповідь, як об’єкт FormData (він буде розглянутий у наступному розділі),
  • response.blob() – повертає відповідь, як Blob (бінарні дані з типом),
  • response.arrayBuffer() – повертає відповідь, як ArrayBuffer (низькорівневе представлення двійкових даних),
  • крім того, response.body це об’єкт ReadableStream, за допомогою якого можна отримувати (зчитувати) тіло відповіді частинами. Такий приклад буде розглянуто трохи пізніше.

Наприклад, буде отримано JSON-об’єкт з останніми комітами із репозиторію GitHub:

let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);

let commits = await response.json(); // прочитати тіло відповіді як JSON

alert(commits[0].author.login);

Те саме буде отримано без await, із використанням промісів:

fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
  .then(response => response.json())
  .then(commits => alert(commits[0].author.login));

Для отримання відповіді у вигляді тексту, використано await response.text() замість .json():

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

let text = await response.text(); // прочитати тіло відповіді як текст

alert(text.slice(0, 80) + '...');

Для прикладу роботи із бінарними даними, буде зроблено запит та виведено на екран логотип специфікації “fetch” (див. розділ Blob, щоб дізнатись детальніше про операції із Blob):

let response = await fetch('/article/fetch/logo-fetch.svg');

let blob = await response.blob(); // скачати, як Blob об'єкт

// створення <img> для нього
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);

// виведення на екран
img.src = URL.createObjectURL(blob);

setTimeout(() => { // приховування через три секунди
  img.remove();
  URL.revokeObjectURL(img.src);
}, 3000);
Важливо:

Можна вибрати тільки один метод читання відповіді.

Якщо, було отримано відповід із response.text(), тоді response.json() не спрацює, бо дані вже були оброблені…

let text = await response.text(); // читаємо тіло відповіді
let parsed = await response.json(); // завершується помилкою, бо дані вже прочитані

Заголовки відповіді

Заоголовки відповіді зберігаются у схожому на Map об’єкті response.headers.

Це не зовсім Map, але можна використовувати такі самі методи, щоб отримати заголовок за його назвою або перебрати заголовки у циклі:

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

// отримання одного заголовку
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8

// перебір усіх заголовків
for (let [key, value] of response.headers) {
  alert(`${key} = ${value}`);
}

Заголовки запиту

Для встановлення заголовка запиту в fetch, можна використати властивість headers в об’єкті options. Вона містит об’єкт з вихідними заголовками, наприклад:

let response = fetch(protectedUrl, {
  headers: {
    Authentication: 'secret'
  }
});

…Але існує список заборонених HTTP заголовків, які не можна встановити:

  • Accept-Charset, Accept-Encoding
  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Connection
  • Content-Length
  • Cookie, Cookie2
  • Date
  • DNT
  • Expect
  • Host
  • Keep-Alive
  • Origin
  • Referer
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade
  • Via
  • Proxy-*
  • Sec-*

Ці заголовки забезпечуют достовірність HTTP, через це вони контролюются і встановлюються лише браузером.

POST запити

Для відправлення POST запиту або запиту з іншим методом, треба використати fetch параметри:

  • method – HTTP-метод, наприклад POST,
  • body – тіло запиту, щось одне із списку:
    • рядок (наприклад, у форматі JSON),
    • об’єкт FormData, для відправки даних як multipart/form-data,
    • Blob/BufferSource для відправлення бінарних даних,
    • URLSearchParams, для відправлення даних у кодуванні x-www-form-urlencoded, використовується рідко.

Частіше використовується JSON формат.

Наприклад, цей код відправляє об’єкт user як JSON:

let user = {
  name: 'John',
  surname: 'Smith'
};

let response = await fetch('/article/fetch/post/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  },
  body: JSON.stringify(user)
});

let result = await response.json();
alert(result.message);

Зверніть увагу, якщо тіло запиту body – рядок, то заголовок Content-Type типово буде text/plain;charset=UTF-8 .

Але, оскільки ми надсилаємо дані у форматі JSON, то через headers ми маємо встановити значення application/json – правильний Content-Type для JSON формату.

Відправлення зображення

Можна відправити бінарні дані за допомогою fetch, використовуючи об’єкт Blob або BufferSource.

У прикладі нижче, є елемент <canvas>, на котрому можна малювати рух мишки. При натисканні на кнопку “відправити”, то зображення буде відправлено на сервер:

<body style="margin:0">
  <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>

  <input type="button" value="Submit" onclick="submit()">

  <script>
    canvasElem.onmousemove = function(e) {
      let ctx = canvasElem.getContext('2d');
      ctx.lineTo(e.clientX, e.clientY);
      ctx.stroke();
    };

    async function submit() {
      let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
      let response = await fetch('/article/fetch/post/image', {
        method: 'POST',
        body: blob
      });

      // сервер відповідає підтвердженням та розміром зображення
      let result = await response.json();
      alert(result.message);
    }

  </script>
</body>

Зауваження, тут не потрібно вручну встановлювати заголовок Content-Type, бо об’єкт Blob вбудований тип (буде використано image/png, заданий через toBlob). Під час відправлення об’єктів Blob, він автоматично стає значенням Content-Type.

Функція submit() може бути переписана без async/await, наприклад наступним чином:

function submit() {
  canvasElem.toBlob(function(blob) {
    fetch('/article/fetch/post/image', {
      method: 'POST',
      body: blob
    })
      .then(response => response.json())
      .then(result => alert(JSON.stringify(result, null, 2)))
  }, 'image/png');
}

Підсумки

Типовий запит за допомогою fetch складається з двох операторів await:

let response = await fetch(url, options); // завершення із заголовками відповіді
let result = await response.json(); // читання тіла у форматі json

Або без await:

fetch(url, options)
  .then(response => response.json())
  .then(result => /* process result */)

Параметри відповіді:

  • response.status – HTTP-статус відповіді,
  • response.oktrue, якщо статус відповіді у діапазоні 200-299.
  • response.headers – схожий на Mapоб’єкт із HTTP заголовками.

Методи для отримання тіла відповіді:

  • response.text() – повертає відповід, як звичайний текст,
  • response.json() – декодує відповідь у форматі JSON,
  • response.formData() – повертає відповідь як об’єкт FormData (кодування multipart/form-data, див. у наступному розділі),
  • response.blob() – повертає об’єкт як Blob (бінарні дані з типом),
  • response.arrayBuffer() – повертає відповідь як ArrayBuffer (низько рівневі бінарні дані),

Опції fetch, які ми розглянули:

  • method – HTTP-метод,
  • headers – об’єкт із заголовками запиту (не всі заголовки дозволені),
  • body – дані для відправлення (тіло запиту) у вигляді тексту string, FormData, BufferSource, Blob або UrlSearchParams об’єкт.

У наступних розділах буде розглянуто більше параметрів та варіантів використання fetch.

Завдання

Створіть асинхронну функцію getUsers(names), яка приймає масив логінів користувачів GitHub, завантажує дані з GitHub та повертає масив об’єктів користувачів GitHub з інформацією про них.

Ось тут можна завантажити дані про користувача Github з усією інформацією за заданим USERNAME: https://api.github.com/users/USERNAME.

У пісочниці є приклад тесту.

Важливі нюанси:

  1. Для кожного користувача повинен бути зроблений один запит fetch.
  2. Запити не повинні чекати один одного. Це дозволяє даним надходити якнайшвидше.
  3. Якщо будь-який запит не вдасться або користувач не існує, функція повинна повернути null у вихідному масиві.

Відкрити пісочницю з тестами.

Щоб отримати інформацію про користувачів, нам потрібно викликати : fetch('https://api.github.com/users/USERNAME').

Якщо відповідь приходить із статусом 200, то викликаємо метод .json(), щоб прочитати JS-об’єкт.

В іншому випадку, якщо fetch завершується помилкою, або код статусу у відповіді відмінний від 200, то просто повертаємо значення null у масиві результатів.

Ось код:

async function getUsers(names) {
  let jobs = [];

  for(let name of names) {
    let job = fetch(`https://api.github.com/users/${name}`).then(
      successResponse => {
        if (successResponse.status != 200) {
          return null;
        } else {
          return successResponse.json();
        }
      },
      failResponse => {
        return null;
      }
    );
    jobs.push(job);
  }

  let results = await Promise.all(jobs);

  return results;
}

Потрібно звернути увагу на те, що виклик .then прикріплений до fetch, щоб коли відповідь отримана, то одразу починати зчитування даних за допомогою .json() не очікуючи завершення інших запитів.

Якщо, було б використано await Promise.all(names.map(name => fetch(...))) та викликали б .json() на результатах запитів, то треба було б чекати поки закінчилися всі запити. Викликаючи .json() одразу після кожного fetch, ми добились того, що зчитування надісланих по кожному окремому запиту відбуваєся незалежно від інших запитів.

Це приклад того, як відносно низько-рівневе Promise API може бути корисним, навіть якщо ми переважно використовуємо async/await.

Відкрити рішення із тестами в пісочниці.

Навчальна карта