Към съдържанието
wannacode

Данни от API

Накратко: fetch взима данни от API с async/await, проверява response.ok, чете JSON и страницата показва едно от четирите състояния: зарежда, грешка, празно или данни.

Проблем

Празна витрина

Страницата на млечните продукти взима продуктите от сървъра. Днес сървърът има проблем и отговаря с грешка 500. Купувачът вижда заглавие и нищо под него.

HTML, CSS, JS

В конзолата пише products.map is not a function. Кодът е приел отговора на грешката, { "error": "…" }, за списък с продукти. Купувачът не вижда нито грешка, нито „опитай пак“. А и когато сървърът работи, докато отговорът пътува, страницата пак е празна.

Идеята

fetch в страницата

fetch(адрес) праща заявка и връща обещание (Promise) за отговора. С await функцията чака отговора, без да спира страницата:

const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
  • fetch хвърля само ако отговор няма: няма мрежа. Отговор с 404 или 500 е отговор, и обещанието се изпълнява.
  • response.ok е true само за кодове от 200 до 299. Проверката е твоя работа.
  • response.json() чете тялото и го превръща в обект. Това също е обещание, затова и то е с await.

В пясъчника адресите /api/… отговаря мнимото API на магазина, без мрежа: GET /api/products (с ?q= и ?category=), /api/products/3, /api/categories, /api/orders и POST /api/orders. Урокът може да го направи бавно, да му каже да отговаря с 500 или да върне празен списък. Така всяко състояние е истинско и винаги еднакво.

Идеята

Четири състояния

Заявката има време и изход. Екранът трябва да казва какво става във всеки момент:

  • зарежда: заявката е тръгнала, отговор няма;
  • данни: дошъл е списък с продукти;
  • празно: дошъл е списък, но празен. Това не е грешка;
  • грешка: няма отговор или отговорът не е ok.

Всяко състояние е елемент в HTML-а. Кодът показва и скрива елементите със свойството hidden:

<p id="loading" role="status">Зареждане…</p>
<p id="error" role="alert" hidden></p>
<p id="empty" hidden>Няма продукти в тази категория.</p>
<ul id="products"></ul>

„Зареждане…“ е видимо от самото начало, още преди JavaScript да тръгне. role="status" кара екранния четец да го прочете, а role="alert" прочита грешката веднага, щом се появи.

Виж го

Виж една заявка

async function load() {
try {
const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
empty.hidden = products.length > 0;
list.replaceChildren(...products.map(productItem));
} catch {
error.textContent = 'Продуктите не се заредиха.';
error.hidden = false;
} finally {
loading.hidden = true;
}
}
Стъпка по стъпка, с думи
  1. load() праща заявката. Отговор още няма: на екрана е „Зареждане…“ от HTML-а.
  2. await чака, но страницата не замръзва: купувачът може да превърта, а спинърът се върти. При бавна мрежа това са секунди.
  3. 200 и масив: ok е true, json() дава продуктите, редовете се рисуват, а finally скрива „Зареждане…“.
  4. Същата заявка, отговор 500. fetch не хвърля: той е получил отговор. Хвърля нашата проверка на ok, а catch показва грешката.
  5. 200 и []: това не е грешка. Масивът е празен, затова страницата казва „няма продукти“ вместо празно място.
  6. Четири състояния, всяко видимо: зарежда, данни, грешка, празно. finally скрива „Зареждане…“ при всеки изход от try.

Същият код, три различни отговора. При 500 fetch не хвърля: хвърля проверката на ok, и catch показва грешката. finally скрива „Зареждане…“ и в трите случая.

Виж го

Бавното API

Тук API-то отговаря след 1,2 секунди. Натисни „Изпълни“ и гледай „Зареждане…“.

HTML, CSS, JS

Целият код на зареждането:

async function load() {
try {
const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
empty.hidden = products.length > 0;
list.replaceChildren(...products.map(productItem));
} catch {
error.textContent = 'Продуктите не се заредиха.';
error.hidden = false;
} finally {
loading.hidden = true;
}
}
load();

Пробвай

Грешка

Тук първата заявка получава 500, а втората минава. Направи така, че при грешка страницата да покаже „Продуктите не се заредиха.“ в #error и бутона „Опитай пак“, „Зареждане…“ да изчезне, а „Няма продукти“ да не се показва. Бутонът вече зарежда наново: провери ли, след него трябва да се видят продуктите.

HTML, CSS, JS

Пробвай

Празно

Тук API-то връща празен списък. Покажи „Няма продукти в тази категория.“, когато продуктите са нула, и го скрий, когато ги има.

HTML, CSS, JS

Пробвай

Намери грешката

Сървърът отговаря с 200 и продукти, а страницата показва грешка.

Избери реда с грешката.

Идеята

Изпращане: бутонът и чакането

Поръчката тръгва с POST. Данните са в тялото като JSON, а заглавката Content-Type казва на сървъра какво получава:

const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, items }),
});

data е обектът от полетата на формуляра: Object.fromEntries(new FormData(form)) прави от двойките name=value обект { name: …, email: …, address: … }.

Докато отговорът пътува, бутонът „Поръчай“ още е включен. Нетърпелив купувач натиска два пъти:

HTML, CSS, JS

Две заявки, две поръчки: 61 и 62. Страницата показва последната и никой не разбира, че първата също е приета.

Пробвай

Един клик, една поръчка

Изключи бутона „Поръчай“, преди заявката да тръгне, и го включи пак, когато отговорът дойде, дори ако е грешка. Проверката натиска два пъти бързо и очаква една поръчка: 61.

HTML, CSS, JS
По-точно, за любопитните

Грешка в мрежата. Без мрежа fetch хвърля TypeError: Failed to fetch. catch хваща и нея, и твоето throw при !response.ok: за купувача и двете са „не се зареди“.

Тялото на грешката. Сървърът често обяснява грешката в тялото, като JSON. При 400 мнимото API връща { "errors": { "email": "…" } }: грешка по поле. Затова в упражнението тялото се чете и при грешка.

Опитай пак. Бутон „Опитай пак“ до грешката просто вика load() наново. Преди това „Зареждане…“ трябва да се покаже отново, а грешката да се скрие.

Състезание между заявки. Ако купувачът смени категорията два пъти бързо, двата отговора може да дойдат в обратен ред и страницата да покаже старата категория. Как се спира стара заявка (AbortController) е в модула за ефекти.

Проверка в браузъра и на сървъра. required и type="email" спират формуляра, преди да тръгне. Сървърът проверява наново, защото заявка може да дойде и без нашата страница. В упражнението полетата са без вградени проверки, за да се види какво отговаря сървърът.

Упражнение

Поръчката

Изпрати поръчката: POST /api/orders с name, email и address от формуляра и items от масива. Докато се праща, бутонът е изключен. При 400 сложи всяко съобщение от errors в абзаца под полето (#email-error и т.н.), а старите съобщения изчисти при всяко ново изпращане. При успех в #result пиши „Поръчка 61 е приета. Общо: 4,00 €“ (номерът и сумата са от отговора).

HTML, CSS, JS

Тест

Провери се

1. Сървърът отговаря с 500. Какво прави await fetch(…)?
2. API-то връща []. Кое условие показва „Няма продукти“?
3. Защо бутонът „Поръчай“ се изключва, преди да тръгне заявката, а не след нея?
4. Защо „Зареждане…“ се скрива във finally?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·