Lekcja 17: Fetch API - Wykonywanie zapytań sieciowych

Współczesne aplikacje internetowe często muszą komunikować się z serwerami, aby pobierać lub wysyłać dane. Historycznie służył do tego obiekt XMLHttpRequest, ale jego API jest uważane za nieco przestarzałe i skomplikowane. Nowoczesnym standardem do wykonywania zapytań sieciowych w JavaScript jest Fetch API.

Fetch API dostarcza globalną funkcję fetch(), która w prosty i elastyczny sposób pozwala na wysyłanie asynchronicznych żądań HTTP i odbieranie odpowiedzi. Bazuje ona na Obietnicach (Promises), co doskonale integruje się z poznanymi wcześniej mechanizmami .then()/.catch() oraz async/await.

Podstawowe użycie fetch()

Najprostsze użycie fetch() polega na przekazaniu adresu URL zasobu, który chcemy pobrać. Domyślnie wykonuje ono żądanie typu GET.

Funkcja fetch() zwraca obietnicę, która rozwiązuje się do obiektu Response reprezentującego odpowiedź serwera (nawet jeśli jest to odpowiedź z błędem HTTP, np. 404 Not Found lub 500 Internal Server Error).

const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1'; // Przykładowe publiczne API

console.log("Wysyłanie żądania fetch...");

fetch(apiUrl)
    .then(response => {
        // Pierwszy .then() otrzymuje obiekt Response
        console.log("Otrzymano odpowiedź (obiekt Response):", response);

        // Sprawdzenie statusu odpowiedzi
        // fetch() nie odrzuca obietnicy dla błędów HTTP (4xx, 5xx)
        // Musimy sami sprawdzić właściwość response.ok lub response.status
        if (!response.ok) {
            // Jeśli status nie jest OK (np. 404, 500), rzucamy błąd
            throw new Error(`Błąd HTTP! Status: ${response.status}`);
        }

        // Odpowiedź trzeba przetworzyć, np. odczytać jako JSON
        // Metody .json(), .text(), .blob() itp. również zwracają Promise!
        console.log("Przetwarzanie odpowiedzi jako JSON...");
        return response.json(); // Zwraca Promise rozwiązujący się do sparsowanego JSON
    })
    .then(data => {
        // Drugi .then() otrzymuje przetworzone dane (w tym przypadku obiekt JSON)
        console.log("Otrzymane dane (JSON):", data);
        // Możemy teraz użyć danych
        // np. document.body.innerHTML = `

${data.title}

${data.body}

`; }) .catch(error => { // .catch() złapie błędy sieciowe (np. brak połączenia) // oraz błędy rzucone przez nas (np. z powodu response.ok === false) console.error("Wystąpił błąd podczas fetch:", error); }); console.log("Żądanie fetch wysłane (operacja asynchroniczna)...");

Ważne: Obietnica zwracana przez fetch() jest odrzucana tylko w przypadku błędów sieciowych uniemożliwiających nawiązanie połączenia (np. problem z DNS, brak połączenia internetowego). Błędy HTTP (jak 404 czy 500) nie powodują odrzucenia obietnicy. Musimy sami sprawdzić status odpowiedzi w pierwszym .then() za pomocą właściwości response.ok (true dla statusów 200-299) lub response.status.

Obiekt Response

Obiekt Response zawiera informacje o odpowiedzi serwera oraz metody do odczytania jej treści (body). Najważniejsze właściwości i metody:

Uwaga: Treść odpowiedzi (body) można odczytać tylko raz za pomocą jednej z metod (.json(), .text() itp.). Próba ponownego odczytania spowoduje błąd.

Konfiguracja żądania (drugi argument fetch)

Funkcja fetch() może przyjąć drugi, opcjonalny argument - obiekt konfiguracyjny, który pozwala dostosować żądanie, np. zmienić metodę HTTP, dodać nagłówki czy wysłać dane.

const postApiUrl = 'https://jsonplaceholder.typicode.com/posts';

const nowyPost = {
    title: 'foo',
    body: 'bar',
    userId: 1,
};

const opcjeFetch = {
    method: 'POST', // Metoda HTTP
    headers: {
        'Content-Type': 'application/json; charset=UTF-8', // Informujemy serwer, że wysyłamy JSON
        // Można dodać inne nagłówki, np. autoryzacyjne
        // 'Authorization': 'Bearer moj_token'
    },
    body: JSON.stringify(nowyPost) // Dane do wysłania (muszą być stringiem!)
};

console.log("Wysyłanie żądania POST...");

fetch(postApiUrl, opcjeFetch)
    .then(response => {
        if (!response.ok) {
            // Sprawdzenie statusu - ważne np. dla POST, PUT, DELETE
            // Status 201 Created jest też OK
            throw new Error(`Błąd HTTP! Status: ${response.status}`);
        }
        // Zazwyczaj odpowiedź na POST zawiera utworzony zasób lub potwierdzenie
        return response.json();
    })
    .then(data => {
        console.log("Odpowiedź serwera na POST:", data);
        // Przykładowa odpowiedź z jsonplaceholder: { id: 101, title: 'foo', ... }
    })
    .catch(error => {
        console.error("Błąd podczas wysyłania POST:", error);
    });

Najczęściej używane opcje w obiekcie konfiguracyjnym:

Fetch z async/await

Użycie fetch ze składnią async/await jest bardzo popularne i często prowadzi do bardziej czytelnego kodu, zwłaszcza przy obsłudze błędów za pomocą try...catch.

async function pobierzDanePosta(postId) {
    const url = `https://jsonplaceholder.typicode.com/posts/${postId}`;
    console.log(`Pobieranie posta ${postId} (async/await)...`);

    try {
        // Oczekiwanie na odpowiedź (obiekt Response)
        const response = await fetch(url);
        console.log("Otrzymano odpowiedź (async/await):", response.status);

        // Sprawdzenie statusu
        if (!response.ok) {
            throw new Error(`Błąd HTTP! Status: ${response.status}`);
        }

        // Oczekiwanie na sparsowanie JSON
        const data = await response.json();
        console.log(`Dane posta ${postId} (async/await):`, data);
        return data;

    } catch (error) {
        console.error(`Błąd podczas pobierania posta ${postId} (async/await):`, error);
        // Można tu rzucić błąd dalej lub zwrócić null/wartość domyślną
        throw error;
    }
}

// Wywołanie funkcji async
(async () => {
    try {
        const post1 = await pobierzDanePosta(1);
        const post5 = await pobierzDanePosta(5);
        console.log("Pobrano posty 1 i 5.");

        // Próba pobrania nieistniejącego posta (spowoduje błąd 404)
        const postNieistniejacy = await pobierzDanePosta(9999);

    } catch (error) {
        // Złapanie błędu rzuconego przez pobierzDanePosta
        console.error("Ostateczny błąd w bloku IIFE:", error.message);
    }
})(); // Użycie IIFE (Immediately Invoked Function Expression) do wywołania async

Zadanie praktyczne

Użyj Fetch API i składni async/await, aby pobrać listę użytkowników z publicznego API: https://jsonplaceholder.typicode.com/users.

Napisz funkcję async o nazwie pobierzUzytkownikow, która:

  1. Wykonuje żądanie GET do podanego URL.
  2. Sprawdza, czy odpowiedź ma status OK (response.ok). Jeśli nie, rzuca błąd.
  3. Parsuje odpowiedź jako JSON.
  4. Zwraca tablicę użytkowników.
  5. Obsługuje ewentualne błędy za pomocą try...catch, wyświetlając komunikat w konsoli i rzucając błąd dalej.

Następnie wywołaj tę funkcję i w bloku .then() (lub w kolejnym await w IIFE) wyświetl w konsoli liczbę pobranych użytkowników oraz imię pierwszego użytkownika z listy. Dodaj też blok .catch() do obsłużenia błędu.

Pokaż rozwiązanie
async function pobierzUzytkownikow() {
    const url = 'https://jsonplaceholder.typicode.com/users';
    console.log("Pobieranie listy użytkowników...");

    try {
        const response = await fetch(url);
        console.log("Status odpowiedzi:", response.status);

        if (!response.ok) {
            throw new Error(`Błąd HTTP! Status: ${response.status}`);
        }

        const uzytkownicy = await response.json();
        console.log("Pobrano dane użytkowników.");
        return uzytkownicy;

    } catch (error) {
        console.error("Błąd podczas pobierania użytkowników:", error);
        throw error; // Rzucamy błąd dalej
    }
}

// Wywołanie i obsługa wyniku
(async () => {
    try {
        const listaUzytkownikow = await pobierzUzytkownikow();
        if (listaUzytkownikow && listaUzytkownikow.length > 0) {
            console.log(`Pobrano ${listaUzytkownikow.length} użytkowników.`);
            console.log(`Imię pierwszego użytkownika: ${listaUzytkownikow[0].name}`);
        } else {
            console.log("Nie pobrano żadnych użytkowników lub lista jest pusta.");
        }
    } catch (error) {
        console.error("Nie udało się przetworzyć danych użytkowników:", error.message);
    }
})();

Zadanie do samodzielnego wykonania

Napisz funkcję async o nazwie dodajNowyPost(tytul, tresc), która użyje Fetch API do wysłania żądania POST na adres https://jsonplaceholder.typicode.com/posts w celu dodania nowego posta.

Funkcja powinna:

  1. Przygotować obiekt danych posta z przekazanymi tytul, tresc i stałym userId: 1.
  2. Przygotować obiekt opcji dla fetch z metodą 'POST', odpowiednimi nagłówkami ('Content-Type': 'application/json') i przekształconym do JSON body.
  3. Wykonać żądanie fetch z użyciem await.
  4. Sprawdzić status odpowiedzi (oczekiwany status sukcesu to 201 Created). Jeśli nie jest OK, rzucić błąd.
  5. Sparować odpowiedź JSON (zawierającą dane utworzonego posta z przypisanym ID) i zwrócić ją.
  6. Obsłużyć błędy za pomocą try...catch.

Wywołaj funkcję z przykładowymi danymi i wyświetl w konsoli odpowiedź serwera (dane utworzonego posta).

FAQ - Fetch API

Czym Fetch API różni się od XMLHttpRequest (XHR)?

Fetch API ma nowocześniejsze i prostsze API oparte na Promises, co ułatwia pracę z kodem asynchronicznym (zwłaszcza z async/await). XHR ma bardziej skomplikowane API oparte na zdarzeniach i callbackach. Fetch lepiej integruje się też z innymi nowoczesnymi standardami webowymi.

Jak obsłużyć timeout w Fetch API?

Standardowo Fetch API nie ma wbudowanej opcji timeout. Najczęstszym sposobem jest użycie AbortController w połączeniu z setTimeout. Tworzymy sygnał przerwania, przekazujemy go do opcji fetch, a po upływie określonego czasu wywołujemy abort() na kontrolerze, co odrzuci obietnicę fetch.

Co to jest CORS i jak wpływa na Fetch?

CORS (Cross-Origin Resource Sharing) to mechanizm bezpieczeństwa przeglądarki, który ogranicza możliwość wykonywania żądań do innego źródła (domeny, protokołu, portu) niż to, z którego pochodzi strona. Aby żądanie Fetch do innego źródła się powiodło, serwer docelowy musi wysłać odpowiednie nagłówki CORS (np. `Access-Control-Allow-Origin`).

Czy mogę wysyłać pliki za pomocą Fetch?

Tak. Najczęściej używa się do tego obiektu FormData. Tworzysz instancję FormData, dodajesz do niej plik (np. z elementu <input type="file">) za pomocą metody append(), a następnie przekazujesz ten obiekt FormData jako body w opcjach fetch. Nie musisz ustawiać nagłówka 'Content-Type', przeglądarka zrobi to automatycznie.

Jak przechwycić i obsłużyć przekierowania (redirects)?

Domyślnie Fetch automatycznie podąża za przekierowaniami HTTP (statusy 3xx). Możesz kontrolować to zachowanie za pomocą opcji redirect w obiekcie konfiguracyjnym ('follow', 'error', 'manual'). Ustawienie 'manual' pozwala uzyskać obiekt Response z typem 'opaqueredirect', ale bez dostępu do szczegółów przekierowania.

Czy Fetch jest dostępne we wszystkich przeglądarkach i Node.js?

Fetch API jest szeroko wspierane we wszystkich nowoczesnych przeglądarkach. W środowisku Node.js, globalna funkcja fetch jest dostępna od wersji 18 (eksperymentalnie od 16.15). W starszych wersjach Node.js konieczne było użycie zewnętrznych bibliotek, takich jak `node-fetch`.