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:


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 debugera 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:

response 404

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

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 interackji. 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<inputFile.files.length; i++) {
    formData.append("photos", inputFile.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:

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.