Fetch API

Ostatnia aktualizacja: 24 lutego 2021

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.eu/rest/v2/name/Poland")
    .then(res => {
        console.log(res);
    })

lub korzystając ze składni async/await:


(async () => {
    const res = await fetch("https://restcountries.eu/rest/v2/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.eu/rest/v2/name/Poland")
    .then(res => res.json())
    .then(res => {
        console.log(res);
    })

(async () => {
    const res = await fetch("https://restcountries.eu/rest/v2/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ść (500), dany adres może nie istnieć (404), albo strona może być przekierowana na inny adres (301).

Aby to sprawdzić spróbujmy połączyć się na błędny adres:


fetch("https://restcountries.eu/rest/v2/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 - 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. nie ma dostępu do internetu), 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.eu/rest/v2/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.eu/rest/v2/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);
    }
})();

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.eu/rest/v2/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. Jeżeli więc dane chcemy wysłać jako proste dane tekstowe, zakodujemy je w formie url i dodamy odpowiedni nagłówek:


function prepareData(dataToCode) {
    const dataPart = [];
    for (let key in dataToCode) {
        dataPart.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
    }
    //w adresach URL spacja występuje w postaci %20, natomiast
    //w danych wysyłanych przez formularze spacja oznaczona jest jako +
    //https://stackoverflow.com/questions/1634271/url-encoding-the-space-character-or-20#answer-1634293
    return dataPart.join("&").replace(/%20/g, "+");
}

const data = {
    name : "Karol Nowak",
    title : "Przykładowy tytuł",
}

const dataToSend = prepareData(data);

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

Podobnie też możemy skorzystać z kodowania multipart/form-data):


const form = document.querySelector("form");
const formData = new FormData(form);

//lub ręcznie dodając dane do formData
const formData = new FormData();
formData.append("name", input1.value);
formData.append("surname", input2.value);

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

Dodatkowy mikro tutorial z używania post:

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę z tego działu, to zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania-ajax

W repozytorium jest branch "solutions". Tam znajdziesz przykładowe rozwiązania.

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.

Menu