Fetch API
Fetch API - nowy interfejs, a także następca XMLHttpRequest, który podobnie do swego brata pozwala pracować z dynamicznymi połączeniami. Głównymi cechami odróżniającymi Fetch od starszego brata jest to, że fetch zwraca nam obietnicę, przez co praca z nim w wielu przypadkach jest zwyczajnie przyjemniejsza.
Pobieranie danych
Podstawowa składnia fetch ma postać:
fetch(url, [options]);
Jedynym wymaganym parametrem jest adres na ktory się łączymy. Jeżeli pominiemy dodatkowe opcje, domyślnie będzie wykonywane połączenie typu GET, które będzie służyć do pobrania danych. Po odpaleniu, fetch zwraca obietnicę, więc tak samo jak w tamtym rozdziale, możemy je skonsumować za pomocą dostępnych metod - then()
, catch()
, all()
itp. reagując tym samym na zakończenie połączenia:
fetch("https://restcountries.com/v3.1/name/Poland")
.then(res => {
console.log(res);
})
lub korzystając ze składni async/await:
(async () => {
const res = await fetch("https://restcountries.com/v3.1/name/Poland");
console.log(res);
})();
Po zwróceniu odpowiedzi mamy dostęp do obiektu Response, które zawiera informacje o wykonanym połączeniu:
ok | czy połączenie zakończyło się sukcesem i możemy zacząć pracować na danych |
---|---|
status | statusy połączenia (200, 404, 301 itp.) |
statusText | status połączenia w formie tekstowej (np. Not found) |
type | typ połączenia (1) |
url | adres na jaki się łączyliśmy |
body | właściwe ciało odpowiedzi |
Właściwa odpowiedź jest przetrzymywana pod właściwością body. W konsoli debuggera odwołanie się do tej właściwości wyświetli nam obiekt ReadableStream. Obiekt ten zawiera naszą odpowiedź, ale nie zawsze będzie ona w pełnej postaci.
Aby uzyskać pełną odpowiedź w interesującym nas formacie powinniśmy zastosować odpowiednią funkcję. W naszym przypadku oczekujemy formatu JSON, więc zastosujmy response.json(). Dla innych typów danych trzeba by użyć innych metod:
response.text() | zwraca odpowiedź w formacie text |
---|---|
response.json() | zwraca odpowiedź jako JSON |
response.formData() | zwraca odpowiedź jako FormData |
response.blob() | zwraca odpowiedź jako blob (dane binarne z tytułem) |
response.arrayBuffer() | zwraca odpowiedź jako ArrayBuffer |
fetch("https://restcountries.com/v3.1/name/Poland")
.then(res => res.json())
.then(res => {
console.log(res);
})
const res = await fetch("https://restcountries.com/v3.1/name/Poland");
const json = await res.json();
console.log(json);
Błędy w połączeniu
Jeżeli łączymy się na poprawny adres i dostajemy poprawną odpowiedź, powyżej wymieniona właściwość ok
ma wartość true, status
takiego połączenia wynosi 200, a my możemy za pomocą then()
działać na zwróconych danych.
Nie zawsze jednak będziemy mieli pewność, że adres na który wykonujemy połączenie zwróci nam prawidłowe dane. Serwer może paść (status 500), albo dany adres może nie istnieć (status 404).
Aby to sprawdzić spróbujmy połączyć się na błędny adres:
fetch("https://restcountries.com/v3.1/name-anka-kaszanka/{name}")
.then(res => {
console.log("Odpowiedź:");
console.dir(res);
})
.catch(error => console.log("Błąd: ", error));
Teoretycznie wystąpił błąd, więc powinna odpalić się funkcja catch()
.
Nic takiego jednak się nie stało - w konsoli debuggera otrzymaliśmy odpowiedź prawie taką samą jak przy naszym pierwszym połączeniu. Różnice są w niektórych właściwościach:

Jak widzimy, status
zmienił się na 404, statusText
na "Not Found", a właściwość ok
zmieniła się na false.
Wynika to z tego, że nasze połączenie zakończyło się powodzeniem (serwer odpowiedział nam jakimiś nagłówkami) - po prostu zrobiliśmy je na adres, który nie istnieje. Jeżeli takiego połączenia w ogóle nie udało by się nawiązać (np. wystąpił błąd sieci), wtedy faktycznie zwrócony by został reject()
, a co za tym idzie - zadziałał by catch().
Jeżeli czytałeś rozdział o XMLHttpRequest, nie powinno cię to w zasadzie dziwić. Dla tamtego obiektu używaliśmy zdarzeń load i error. Load oznaczało zakończenie połączenia. Tak czy siak musieliśmy dodatkowo sprawdzić czy status połączenia równał się 200. Zdarzenie error natomiast odpalane było w przypadku błędu połączenia.
Podobnie działa to w przypadku fetch.
Idąc więc za krokiem - aby dodatkowo obsłużyć potencjalne błędy przy naszych połączeniach, powinniśmy wykonać dodatkowe testy:
fetch("https://restcountries.com/v3.1/name-anka-kaszanka/Poland")
.then(res => {
if (res.ok) {
return res.json()
} else {
return Promise.reject(`Http error: ${res.status}`);
//lub rzucając błąd
//throw new Error(`Http error: ${res.status}`);
}
})
.then(res => {
console.log(res)
})
.catch(error => {
console.error(error)
});
(async () => {
try {
const res = await fetch("https://restcountries.com/v3.1/name-anka-kaszanka/Poland")
if (!res.ok) {
throw new Error(`Http error: ${res.status}`);
}
const json = await res.json();
console.log(json);
} catch (error) {
console.error(error);
}
})();
Ale trzeba tu zaznaczyć, że nawet z powyższym uwzględnieniem błędów, w wielu sytuacjach nie powinno się tego traktować jako w pełni wystarczający kod. Czego tu brakuje? Mikro interakcji. A to pokazania i schowania loading. A to pokazania użytkownikowi komunikatu o błędzie wczytywania czy zbyt długim wczytywaniu. Jako dobry programista nigdy nie powinieneś pomijać takich detali.
Dodatkowe opcje dla fetch
Drugim parametrem fetch jest obiekt, który pozwala nam przekazywać dodatkowe opcje.
fetch("...", {
method: 'POST', //*GET, POST, PUT, DELETE, etc.
mode: 'cors', //no-cors, *cors, same-origin
cache: 'no-cache', //*default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', //include, *same-origin, omit
headers: {
'Content-Type': 'application/json'
//'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *client
body: JSON.stringify(data) //treść wysyłana
})
W większości przypadków interesować nas będzie głównie właściwość headers
, za pomocą której będziemy ustawiać odpowiednie nagłówki oraz właściwość body
, do której przekażemy przesyłaną treść.
Nagłówki
Jeżeli chcemy ustawić jakiś nagłówek wysyłając dane, powinniśmy ustawić je dla klucza headers:
fetch("...", {
headers : {
"Content-Type": "application/json"
}
})
Nagłówki takie możemy ustawiać jak powyżej (i zapewne tak będziemy robić najczęściej), ale możemy też skorzystać z interfejsu Headers(), który udostępnia nam dodatkowe metody do manipulacji pojedynczymi nagłówkami:
append(key, value) | Dodaje nową wartość o danym kluczu do zbioru nagłówków |
---|---|
delete(key) | Usuwa wartość o danym kluczu |
entries() | Zwraca iterator, który umożliwia zrobienie pętli po wszystkich parach klucz/wartość |
get(key) | Zwraca pierwszy nagłówek o danym kluczu |
getAll(key) | Zwraca tablicę wszystkich nagłówki o danym kluczu |
has(key) | Sprawdza czy w zbiorze istnieje wartość o danym kluczu |
set(key) | Ustawia nową wartość dla danego klucza |
keys() | Zwraca listę kluczy z danego FormData |
values() | Zwraca listę wartości z danego FormData |
const url = "https://jsonplaceholder.typicode.com/users";
const ob = {
name: "John",
surname: "Connor",
email: "dead@replacedbydani.com"
}
const headers = new Headers();
headers.append("Content-Type", "application/json");
fetch(url, {
method: "post",
headers: headers,
body: JSON.stringify(ob)
})
.then(res => res.json())
.then(res => {
console.log("Dodałem użytkownika:");
console.log(res);
})
Możemy też skorzystać z bardziej skróconego zapisu:
const url = "https://jsonplaceholder.typicode.com/users";
const ob = {
name: "John",
surname: "Connor",
email: "dead@replacedbydani.com"
}
fetch(url, {
method: "post",
headers: new Headers([
"Content-Type": "text/plain"
]),
body: JSON.stringify(ob)
})
.then(res => res.json())
.then(res => {
console.log("Dodałem użytkownika:");
console.log(res);
})
Nie wszystkie nagłówki będziemy mogli ustawić w ten sposób.
Do nagłówków, które przyszły wraz z odpowiedzią (response headers) mamy dostęp przez właściwość response.headers:
fetch("https://restcountries.com/v3.1/name/Poland")
.then(res => {
console.log(res.headers.get("Content-Type")); //application/json; charset=utf-8
for (let [key, value] of res.headers) {
console.log(`${key} = ${value}`);
}
})
Wysyłanie danych
Żeby wysłać dane musimy je ustawić we właściwości body oraz ustawić odpowiedni dla nich nagłówek za pomocą właściwości headers.
Działanie jest tutaj podobne jak w przypadku wysyłania danych za pomocą XMLHttpRequest.
Ponownie format danych zależny jest od metody kodowania jaką wybierzemy, ale też od danych jakie wysyłamy.
Jeżeli więc chcemy wysłać proste dane tekstowe, możemy zakodować je do formatu application/x-www-form-urlencoded
(oraz ustawić odpowiedni nagłówek):
//funkcja którą wykorzystaliśmy w rozdziale o XMLHttpRequest
function prepareData(dataToCode) {
const dataPart = [];
for (let key in dataToCode) {
dataPart.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
}
return dataPart.join("&").replace(/%20/g, "+");
}
const data = {
name : "Karol Nowak",
title : "Przykładowy tytuł",
}
const dataToSend = prepareData(data);
const url = "przykładowy-adres-na-serwer.pl";
fetch(url, {
method: "post",
headers: {
"Content-type" : "application/x-www-form-urlencoded"
},
body: dataToSend
})
.then(res => res.json())
.then(res => {
console.log("Dodałem użytkownika:");
console.log(res);
})
Jeżeli wysyłane dane są czymś więcej niż tylko prostymi parami klucz-wartość (np. zawierają pliki), powinniśmy skorzystać z kodowania multipart/form-data), a dane zakodować korzystając z interfejsu FormData()
:
const url = "https://przykładowy-adres-na-serwer.pl";
const form = document.querySelector("form");
const inputName = form.querySelector("input[name=name]");
const inputPhotos = form.querySelector("input[type=file][multiple]");
//lub ręcznie dodając dane do formData
const formData = new FormData();
formData.append("name", inputName.value);
for (let i=0; i<inputPhotos.files.length; i++) {
formData.append("photos", inputPhotos.files[i]);
}
fetch(url, {
method: "post",
body: formData,
//nagłówka nie muszę ustawiać
})
.then(res => res.json())
.then(res => {
console.log("Dodałem użytkownika:");
console.log(res);
})
Najczęściej w komunikacji z wszelakimi API będziemy wysyłać dane w postaci JSON.
W takim przypadku musimy ustawić odpowiedni nagłówek, a same dane zakodować za pomocą JSON.stringify()
:
const ob = {
name : "Piotrek",
age : 10,
pet : {
type : "ultra dog",
speed: 1000,
power : 9001
}
}
fetch("...", {
method: "post",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(ob)
})
.then(res => res.json())
.then(res => {
console.log("Dodałem użytkownika:");
console.log(res);
})
Dodatkowe materiały
Dodatkowy mikro tutorial z używania fetch:
Trening czyni mistrza
Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania