Fetch API

Nowe przeglądarki udostępniają nam Fetch API - nowy interfejs do dynamicznego pobierania zasobów.

Jako, że fetch jest dość nowy, w poniższych skryptach będziemy wykorzystywać nowe rozwiązania takie jak funkcja strzałkowa, template string itp.

Pobieranie danych

Do naszych testów skorzystamy z darmowego API mieszczącego się pod adresem https://jsonplaceholder.typicode.com/.

Po wejściu na tą stronę widzimy, że aby pobrać użytkowników, musimy połączyć się na adres https://jsonplaceholder.typicode.com/users.

Wykonajmy podstawowe połączenie w celu pobrania danych:


fetch("https://jsonplaceholder.typicode.com/users")
    .then(resp => {
        console.log(resp);
    })

Po odpaleniu fetch zwraca Promise, więc tak samo jak w rozdziale o Promise, możemy je obsłużyć za pomocą dostępnych dla Promisów metod - then, all i catch.


{
    const btn = document.querySelector("#btn1");
    btn.addEventListener("click", function() {
        fetch("https://jsonplaceholder.typicode.com/users")
            .then(resp => {
                console.log("Przykład 1:");
                console.log(resp);
            })
    })
}

Po wywołaniu powyższego skryptu naszym oczom w konsoli pokaże się mniej więcej coś takiego:

response fetch

Czyli dostaliśmy odpowiedź. Jak widzisz, wśród właściwości mamy status 200, statusText, url itp.

Po takiej odpowiedzi będziemy mieli dostęp do różnych jej właściwości:


fetch("https://jsonplaceholder.typicode.com/users").then(resp => {
    console.log(resp.headers.get("Content-Type"));
    console.log(resp.headers.get("Date"));

    console.log(resp.status);
    console.log(resp.statusText);
    console.log(resp.type);
    console.log(resp.url);
    console.log(resp.body);
    ...
});

Właściwa odpowiedź jest przetrzymywana pod właściwością body. W konsoli debugera powyższy kod wyświetli nam obiekt ReadableStream.
Aby zamienić go na odpowiedni format musimy zastosować odpowiednią metodę, która skonwertuje tą właściwość na dany format. W naszym przypadku oczekujemy json, więc zastosujmy metodę response.json(). Dla innych typów danych trzeba by użyć innych metod - np. dla tekstu response.text(), a dla grafik i różnego rodzaju plików response.blob():


fetch("https://jsonplaceholder.typicode.com/users")
    .then(resp => resp.json())
    .then(resp => {
        console.log("Przykład 2:");
        console.log(resp);
    })

Naszym oczom w konsoli debugera ukaże się lista użytkowników. Zróbmy więc po niej prostą pętlę:


fetch("https://jsonplaceholder.typicode.com/users")
    .then(resp => resp.json())
    .then(resp => {
        console.log(resp);

        resp.forEach(user => {
            console.groupCollapsed(`Użytkownik ${user.id}`)
            console.log(`Nazwa: ${user.name}`);
            console.log(`Nazwa użytkownika: ${user.username}`);
            console.log(`Email: ${user.email}`);
            console.log(`Adres: ${user.address.city} ${user.address.street} ${user.address.zipcode}`);
            console.log(`WWW: ${user.website}`);
            console.groupEnd();
        })
    })

Swoją drogą powyższy przykład można by zapisać za pomocą destrukturyzacji:


...
resp.forEach(user => {
    const {
        id,
        name = "Brak danych",
        userName : username = "Brak danych",
        email = "Brak danych"
        address : {
            street,
            zipCode : zipcode
        },
        www : website
    } = user;

    console.groupCollapsed(`Użytkownik ${id}`)
    console.log(`Nazwa: ${name}`);
    console.log(`Nazwa użytkownika: ${userName}`);
    console.log(`Email: ${email}`);
    console.log(`Adres: ${address.city} ${address.street} ${address.zipCode}`);
    console.log(`WWW: ${www}`);
    console.groupEnd();
})
...

Wysyłanie danych

Żeby wysłać dane musimy je ustawić we właściwości body. Dane takie podobnie jak w przypadku XMLHttpRequest powinniśmy zakodowane w ciąg znaków.


fetch("...", {
        method: "post",
        body: "name=Marcin&surname=Nowak"
    })
    .then(res => res.json())
    .then(res => {
        console.log("Dodałem użytkownika:");
        console.log(res);
    })

Jeżeli nie chcemy ręcznie kodować takiego zapisu, skorzystajmy z formData:


const formData = new FormData();
formData.append("name", nameVal);
formData.append("surname", surnameVal);

fetch("...", {
        method: "post",
        body: formData
    })
    .then(res => res.json())
    .then(res => {
        console.log("Dodałem użytkownika:");
        console.log(res);
    })

Sprawdźmy to w praktyce. Po wejściu na stronę https://jsonplaceholder.typicode.com/posts, widzimy, że każdy post składa się z id, title, userId i body. ID jest automatycznie zwiększane, więc musimy wysłać tylko pozostałe właściwości:


const formData = new FormData();
formData.append("title", "Lorem ipsum");
formData.append("body", "Lorem ipsum dolor sit amet consectetur...");
formData.append("userId", 1);

fetch("https://jsonplaceholder.typicode.com/posts", {
        method: "post",
        body: formData
    })
    .then(res => res.json())
    .then(res => {
        console.log("Dodałem użytkownika:");
        console.log(res);
    })

Api do którego się łączymy nie zapisuje realnie użytkowników, ale tylko symuluje. W odpowiedzi dostaliśmy użytkownika o id:101, co oznacza, że zostało symulowane dodanie do bazy nowego użytkownika (normalnie było ich 100).

Czasami zajdzie potrzeba wysłania danych innego typu - np. JSON. Musimy wtedy ustawić odpowiedni nagłówek Content-Type, a nasze dane musimy zakodować do postaci JSON.


const ob = {
    title: "Nazwa posta",
    body: "Lorem ipsum dolor sit amet consectetur...",
    userId: 1
};

fetch("https://jsonplaceholder.typicode.com/posts", {
        method: "post",
        headers: {
            "Content-type": "application/json; charset=UTF-8"
        },
        body: JSON.stringify(ob)
    })
    .then(res => res.json())
    .then(res => {
        console.log("Dodałem użytkownika:");
        console.log(res);
    })

Nagłówki takie możemy ustawić tak jak powyżej - ręcznie. Możemy też skorzystać z obiektu typu Header, który udostępnia nam dodatkowe metody do manipulacji pojedynczymi nagłówkami:


const ourHeaders = new Headers();

//dodajemy dodatkowe nagłówki
ourHeaders.append("Content-Type", "text/plain");
ourHeaders.append("X-My-Custom-Header", "CustomValue");

//czy powyższy obiekt ma dany nagłówek
ourHeaders.has("Content-Type"); // true

//pobieramy dany nagłówek
ourHeaders.get("Content-Type"); // "text/plain"

//ustawiamy nagłówek
ourHeaders.set("Content-Type", "application/json");

//usuwamy nagłówek
ourHeaders.delete("X-My-Custom-Header");


fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "post",
    headers: ourHeaders,
    body: JSON.stringify(ob)
})

Po drugie musimy zamienić nasz obiekt na odpowiedni zapis. Wykorzystujemy tutaj JSON.stringify(). Podobną metodę stosowaliśmy do zapisu obiektów do customowych atrybutów

Błędy w połączeniu

Spróbujmy na początek połączyć się na błędny adres:


fetch("https://jsonplaceholder.typicode.com/kaszanka")
    .then(resp => {
        console.log("Odpowiedź:");
        console.dir(resp)
    })
    .catch(error => console.log("Błąd: ", error));

Teoretycznie wystąpił błąd, więc powinien się odpalić catch. Nic takiego jednak się nie stało, bo w konsoli debugera otrzymaliśmy odpowiedź prawie 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 Promise zwracany przez Fetch nie zwraca reject gdy status połączenia jest niewłaściwy (404, 500 itp).

Aby obsłużyć błędne zapytania, musimy w then() obsłużyć więc powyższe właściwości:


fetch("https://jsonplaceholder.typicode.com/kaszanka")
    .then(resp => {
        if (resp.ok) {
            return response.json()
        } else {
            throw new Error("Wystąpił błąd połączenia!")
        }
    })
    .then(resp => {
        console.log(resp)
    })
    .catch(error => console.dir("Błąd: ", error));

Żeby jeszcze dokładniej poinformować użytkownika o wynikłym błędzie, możemy skorzystać z Promise, który po zwróceniu reject przeskoczy do catch:


fetch("https://jsonplaceholder.typicode.com/kaszanka")
    .then(resp => {
        if (resp.ok) {
            return response.json()
        } else {
            return Promise.reject(resp)
        }
    })
    .then(resp => {
        console.log(resp)
    })
    .catch(error => {
        if (error.status === 404) {
            console.log("Błąd: żądany adres nie istnieje");
        }
    });

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Przejdź na stronę https://developers.google.com/books/docs/v1/using#WorkingVolumes
    Za pomocą fetch pobierz listę książek o tematyce "Wiedźmin".

    Zrób pętlę po wynikach i wrzuć ładnie sformatowane dane do html. Dane niech zawierają:
    - tytuł książki
    - autorzy
    - liczbę podstron
    - link do poglądu
    - czy dostępne w formie pdf

    Do formatowania danych użyj template strings