2 червня 2023 р.
Цей матеріал доступний лише такими мовами: English, Español, Français, Indonesia, Italiano, 日本語, 한국어, Русский, 简体中文. Будь ласка, допоможіть нам перекласти на Українська.

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(); // read response body and parse as 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(); // read response body as 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()` не спрацює, бо дані вже були оброблені..

```js
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.

Завдання

Create an async function getUsers(names), that gets an array of GitHub logins, fetches the users from GitHub and returns an array of GitHub users.

The GitHub url with user information for the given USERNAME is: https://api.github.com/users/USERNAME.

There’s a test example in the sandbox.

Important details:

  1. There should be one fetch request per user.
  2. Requests shouldn’t wait for each other. So that the data arrives as soon as possible.
  3. If any request fails, or if there’s no such user, the function should return null in the resulting array.

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

To fetch a user we need: fetch('https://api.github.com/users/USERNAME').

If the response has status 200, call .json() to read the JS object.

Otherwise, if a fetch fails, or the response has non-200 status, we just return null in the resulting array.

So here’s the code:

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;
}

Please note: .then call is attached directly to fetch, so that when we have the response, it doesn’t wait for other fetches, but starts to read .json() immediately.

If we used await Promise.all(names.map(name => fetch(...))), and call .json() on the results, then it would wait for all fetches to respond. By adding .json() directly to each fetch, we ensure that individual fetches start reading data as JSON without waiting for each other.

That’s an example of how low-level Promise API can still be useful even if we mainly use async/await.

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

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

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)