Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

API Fetch

W nowoczesnym Parku Jurajskim, oprócz lokalnych systemów monitorowania i kontroli, potrzebujemy również komunikować się z zewnętrznymi systemami i API. Czy to sprawdzanie prognoz pogodowych dla parku, synchronizacja danych z globalnymi bazami dinozaurów czy integracja z systemami rezerwacji dla odwiedzających - wszystko to wymaga solidnej metody wykonywania żądań HTTP. W tym ćwiczeniu poznamy API Fetch, nowoczesny interfejs do wykonywania żądań sieciowych w JavaScript.

Czym jest Fetch API?

Fetch API to nowoczesny, wbudowany interfejs JavaScript do wykonywania żądań HTTP. Zastępuje starsze podejścia, takie jak XMLHttpRequest, oferując bardziej ergonomiczny i potężny zestaw narzędzi oparty na Promise.

Główne zalety Fetch API:

  • Korzysta z Promise, co ułatwia obsługę operacji asynchronicznych
  • Ma prostszą i bardziej intuicyjną składnię niż XMLHttpRequest
  • Oferuje lepszą kontrolę nad żądaniami HTTP
  • Pozwala na łatwe łańcuchowanie operacji asynchronicznych
  • Jest standardem przeglądarek i dostępne również w Node.js (od wersji 18)

Podstawowe użycie Fetch

Podstawowa składnia Fetch jest bardzo prosta:

1fetch(url)
2  .then(response => response.json())
3  .then(data => console.log(data))
4  .catch(error => console.error('Błąd:', error));

Przyjrzyjmy się, jak moglibyśmy wykorzystać Fetch w naszym Parku Jurajskim:

1// Pobieranie danych o dinozaurze z API
2function pobierzDaneDinozaura(id) {
3  return fetch(`https://api.jurassic-park.com/dinosaurs/${id}`)
4    .then(response => {
5      // Sprawdzenie, czy odpowiedź jest poprawna
6      if (!response.ok) {
7        throw new Error(`Błąd HTTP: ${response.status}`);
8      }
9      return response.json();
10    })
11    .then(data => {
12      console.log(`Pobrano dane dinozaura: ${data.name}`);
13      return data;
14    })
15    .catch(error => {
16      console.error(`Nie udało się pobrać danych dinozaura #${id}:`, error);
17      throw error;
18    });
19}
20
21// Użycie funkcji
22pobierzDaneDinozaura(42)
23  .then(dino => {
24    console.log(`Dinozaur #42: ${dino.name}, Gatunek: ${dino.species}`);
25    aktualizujPanelInformacyjny(dino);
26  })
27  .catch(error => {
28    wyświetlKomunikatBłędu(`Nie udało się pobrać informacji o dinozaurze: ${error.message}`);
29  });

Wykonywanie różnych typów żądań HTTP

Fetch domyślnie wykonuje żądanie GET, ale możemy łatwo określić inny typ żądania i przesłać dane:

1// Aktualizacja danych dinozaura za pomocą żądania PUT
2async function aktualizujDaneDinozaura(id, noweDane) {
3  try {
4    const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, {
5      method: 'PUT',
6      headers: {
7        'Content-Type': 'application/json',
8        'Authorization': `Bearer ${apiToken}`
9      },
10      body: JSON.stringify(noweDane)
11    });
12
13    if (!response.ok) {
14      throw new Error(`Błąd podczas aktualizacji: ${response.status}`);
15    }
16
17    const zaktualizowanyDinozaur = await response.json();
18    console.log(`Zaktualizowano dane dinozaura #${id}`);
19    return zaktualizowanyDinozaur;
20  } catch (error) {
21    console.error(`Błąd aktualizacji dinozaura #${id}:`, error);
22    throw error;
23  }
24}
25
26// Tworzenie nowego rekordu dinozaura za pomocą żądania POST
27async function dodajNowegoDinozaura(dane) {
28  try {
29    const response = await fetch('https://api.jurassic-park.com/dinosaurs', {
30      method: 'POST',
31      headers: {
32        'Content-Type': 'application/json',
33        'Authorization': `Bearer ${apiToken}`
34      },
35      body: JSON.stringify(dane)
36    });
37
38    if (!response.ok) {
39      throw new Error(`Błąd podczas dodawania dinozaura: ${response.status}`);
40    }
41
42    const nowyDinozaur = await response.json();
43    console.log(`Dodano nowego dinozaura #${nowyDinozaur.id}`);
44    return nowyDinozaur;
45  } catch (error) {
46    console.error('Błąd dodawania dinozaura:', error);
47    throw error;
48  }
49}
50
51// Usunięcie dinozaura za pomocą żądania DELETE
52async function usuńDinozaura(id) {
53  try {
54    const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, {
55      method: 'DELETE',
56      headers: {
57        'Authorization': `Bearer ${apiToken}`
58      }
59    });
60
61    if (!response.ok) {
62      throw new Error(`Błąd podczas usuwania dinozaura: ${response.status}`);
63    }
64
65    // W przypadku DELETE serwer może nie zwracać danych
66    if (response.status !== 204) { // No Content
67      const data = await response.json();
68      console.log(`Usunięto dinozaura #${id}:`, data);
69      return data;
70    }
71
72    console.log(`Usunięto dinozaura #${id}`);
73    return { success: true, id };
74  } catch (error) {
75    console.error(`Błąd usuwania dinozaura #${id}:`, error);
76    throw error;
77  }
78}

Obiekt Response

Kiedy wywołujemy funkcję

fetch()
, zwraca ona Promise, który rozwiązuje się do obiektu
Response
. Ten obiekt zawiera wszystkie informacje o odpowiedzi HTTP:

1fetch('https://api.jurassic-park.com/status')
2  .then(response => {
3    console.log('Status:', response.status); // np. 200
4    console.log('Status OK:', response.ok); // true dla statusu 200-299
5    console.log('Typ zawartości:', response.headers.get('content-type'));
6    console.log('Wszystkie nagłówki:', [...response.headers.entries()]);
7
8    return response.json(); // Parsowanie odpowiedzi jako JSON
9  })
10  .then(data => {
11    console.log('Dane:', data);
12  });

Obiekt

Response
oferuje różne metody do przetwarzania danych odpowiedzi:

| Metoda | Opis | |--------|------| | response.json() | Parsuje zawartość jako JSON i zwraca Promise z wynikiem | | response.text() | Zwraca Promise z zawartością jako tekst | | response.blob() | Zwraca Promise z zawartością jako obiekt Blob (dane binarne) | | response.formData() | Parsuje zawartość jako FormData | | response.arrayBuffer() | Zwraca Promise z zawartością jako ArrayBuffer |

Ważne jest, aby pamiętać, że możesz wywołać tylko jedną z tych metod na obiekcie

Response
. Body odpowiedzi może być przetworzone tylko raz, a próba wielokrotnego wywołania np.
response.json()
spowoduje błąd.

Obsługa błędów w Fetch

Ważną cechą Fetch API jest to, że Promise zwrócony przez

fetch()
nie zostanie odrzucony dla błędów HTTP, takich jak 404 lub 500. Zostanie odrzucony tylko w przypadku problemów sieciowych, takich jak brak połączenia.

Aby prawidłowo obsługiwać błędy HTTP, należy sprawdzić właściwość

response.ok
lub kod statusu
response.status
:

1fetch('https://api.jurassic-park.com/dinosaurs/999')
2  .then(response => {
3    if (!response.ok) {
4      // Tworzenie błędu z kodem statusu
5      throw new Error(`Błąd HTTP: ${response.status}`);
6    }
7    return response.json();
8  })
9  .then(data => {
10    console.log('Dane:', data);
11  })
12  .catch(error => {
13    console.error('Błąd:', error.message);
14    // Tutaj możemy obsłużyć zarówno błędy HTTP, jak i błędy sieci
15  });

Można też rozszerzyć tę logikę, aby lepiej obsługiwać różne kody statusu:

1fetch('https://api.jurassic-park.com/dinosaurs/999')
2  .then(response => {
3    if (response.status === 404) {
4      throw new Error('Dinozaur nie znaleziony');
5    } else if (response.status === 401) {
6      throw new Error('Brak autoryzacji - zaloguj się ponownie');
7    } else if (response.status === 403) {
8      throw new Error('Brak uprawnień do tej operacji');
9    } else if (!response.ok) {
10      throw new Error(`Błąd HTTP: ${response.status}`);
11    }
12    return response.json();
13  })
14  .then(/* ... */)
15  .catch(/* ... */);

Opcje konfiguracyjne Fetch

Fetch przyjmuje opcjonalny drugi argument, który pozwala na dostosowanie żądania:

1fetch(url, {
2  method: 'POST',           // Metoda HTTP (GET, POST, PUT, DELETE, itd.)
3  headers: {                // Nagłówki HTTP
4    'Content-Type': 'application/json',
5    'Authorization': 'Bearer token123'
6  },
7  body: JSON.stringify(data), // Ciało żądania (dla POST, PUT, itd.)
8  mode: 'cors',             // Tryb CORS
9  credentials: 'include',   // Czy dołączać ciasteczka
10  cache: 'no-cache',        // Sposób obsługi cache
11  redirect: 'follow',       // Sposób obsługi przekierowań
12  referrer: 'no-referrer',  // Nagłówek referrer
13  signal: abortController.signal // Do anulowania żądania
14})

Przykład z większością opcji

1async function wykonajZłożoneŻądanie() {
2  const abortController = new AbortController();
3  const signal = abortController.signal;
4
5  // Ustawienie timeout dla żądania
6  const timeout = setTimeout(() => {
7    abortController.abort();
8  }, 5000); // 5 sekund
9
10  try {
11    const response = await fetch('https://api.jurassic-park.com/data', {
12      method: 'POST',
13      headers: {
14        'Content-Type': 'application/json',
15        'Authorization': `Bearer ${apiToken}`,
16        'X-Jurassic-Version': '1.2'
17      },
18      body: JSON.stringify({
19        query: 'velociraptor',
20        limit: 10,
21        withDetails: true
22      }),
23      mode: 'cors',
24      credentials: 'include', // Dołącz ciasteczka
25      cache: 'no-cache',      // Nie używaj cache
26      redirect: 'follow',     // Automatycznie podążaj za przekierowaniami
27      referrer: 'no-referrer',
28      signal                  // Do anulowania żądania
29    });
30
31    clearTimeout(timeout);
32
33    if (!response.ok) {
34      throw new Error(`Błąd HTTP: ${response.status}`);
35    }
36
37    return await response.json();
38  } catch (error) {
39    clearTimeout(timeout);
40
41    if (error.name === 'AbortError') {
42      console.error('Żądanie przerwane po upływie czasu oczekiwania');
43      throw new Error('Przekroczono czas oczekiwania na odpowiedź');
44    }
45
46    console.error('Błąd żądania:', error);
47    throw error;
48  }
49}

Anulowanie żądań z AbortController

AbortController to interfejs, który pozwala na anulowanie żądań Fetch. Jest szczególnie przydatny do implementacji timeoutów lub gdy użytkownik chce anulować długotrwałe żądanie:

1function pobierzDużeDaneDinozaurów(query) {
2  // Utworzenie kontrolera anulowania
3  const controller = new AbortController();
4  const { signal } = controller;
5
6  // Ustawienie timeoutu
7  const timeoutId = setTimeout(() => {
8    controller.abort();
9  }, 10000); // 10 sekund
10
11  // Przycisk anulowania dla użytkownika
12  const cancelButton = document.getElementById('cancel-button');
13  cancelButton.addEventListener('click', () => {
14    controller.abort();
15    clearTimeout(timeoutId);
16    cancelButton.disabled = true;
17    cancelButton.textContent = 'Anulowano';
18  });
19
20  // Wykonanie żądania z sygnałem anulowania
21  return fetch(`https://api.jurassic-park.com/search?q=${query}`, { signal })
22    .then(response => {
23      clearTimeout(timeoutId);
24      if (!response.ok) {
25        throw new Error(`Błąd HTTP: ${response.status}`);
26      }
27      return response.json();
28    })
29    .then(data => {
30      cancelButton.disabled = true;
31      cancelButton.textContent = 'Zakończono';
32      return data;
33    })
34    .catch(error => {
35      clearTimeout(timeoutId);
36
37      if (error.name === 'AbortError') {
38        console.log('Żądanie zostało anulowane');
39        // Obsługujemy anulowanie jako normalny przepływ, nie jako błąd
40        return { cancelled: true };
41      }
42
43      // Dla innych błędów, przekazujemy dalej
44      throw error;
45    });
46}

Równoległe żądania z Promise.all

Czasami potrzebujemy wykonać wiele żądań jednocześnie. Możemy to zrobić łącząc Fetch z Promise.all:

1async function pobierzDaneDlaWybiegu(wybiegId) {
2  try {
3    // Definiujemy wszystkie żądania
4    const dinozauryPromise = fetch(`https://api.jurassic-park.com/paddocks/${wybiegId}/dinosaurs`).then(r => r.json());
5    const statusPromise = fetch(`https://api.jurassic-park.com/paddocks/${wybiegId}/status`).then(r => r.json());
6    const historiaPromise = fetch(`https://api.jurassic-park.com/paddocks/${wybiegId}/history`).then(r => r.json());
7
8    // Wykonujemy wszystkie żądania równolegle
9    const [dinozaury, status, historia] = await Promise.all([
10      dinozauryPromise,
11      statusPromise,
12      historiaPromise
13    ]);
14
15    // Łączymy dane w jeden obiekt
16    return {
17      wybiegId,
18      dinozaury,
19      status,
20      historia,
21      aktualizacja: new Date()
22    };
23  } catch (error) {
24    console.error(`Błąd pobierania danych dla wybiegu #${wybiegId}:`, error);
25    throw error;
26  }
27}

Obsługa różnych formatów odpowiedzi

Fetch może obsługiwać różne formaty danych, nie tylko JSON:

Tekst zwykły

1fetch('https://api.jurassic-park.com/logs')
2  .then(response => response.text())
3  .then(text => {
4    console.log('Logi:', text);
5    document.getElementById('logs').textContent = text;
6  });

Dane binarne (obrazy, pliki)

1// Pobieranie obrazu i wyświetlanie go
2fetch('https://api.jurassic-park.com/dinosaurs/42/image')
3  .then(response => response.blob())
4  .then(blob => {
5    const imageUrl = URL.createObjectURL(blob);
6    document.getElementById('dino-image').src = imageUrl;
7  });
8
9// Pobieranie i zapisywanie pliku (w środowisku przeglądarki)
10fetch('https://api.jurassic-park.com/reports/monthly.pdf')
11  .then(response => response.blob())
12  .then(blob => {
13    const url = URL.createObjectURL(blob);
14    const a = document.createElement('a');
15    a.href = url;
16    a.download = 'raport-miesięczny.pdf';
17    document.body.appendChild(a);
18    a.click();
19    document.body.removeChild(a);
20    URL.revokeObjectURL(url);
21  });

Wysyłanie formularzy

Fetch może również obsługiwać wysyłanie formularzy:

1// Wysyłanie danych formularza jako JSON
2document.getElementById('dino-form').addEventListener('submit', async function(e) {
3  e.preventDefault();
4
5  const form = e.target;
6  const formData = new FormData(form);
7
8  // Konwersja FormData do obiektu JavaScript
9  const formObject = {};
10  for (const [key, value] of formData.entries()) {
11    formObject[key] = value;
12  }
13
14  try {
15    const response = await fetch('https://api.jurassic-park.com/dinosaurs', {
16      method: 'POST',
17      headers: {
18        'Content-Type': 'application/json'
19      },
20      body: JSON.stringify(formObject)
21    });
22
23    if (!response.ok) {
24      throw new Error(`Błąd HTTP: ${response.status}`);
25    }
26
27    const result = await response.json();
28    alert(`Dinozaur dodany pomyślnie! ID: ${result.id}`);
29    form.reset();
30  } catch (error) {
31    console.error('Błąd podczas wysyłania formularza:', error);
32    alert(`Błąd: ${error.message}`);
33  }
34});
35
36// Wysyłanie formularza z plikami
37document.getElementById('dino-upload-form').addEventListener('submit', async function(e) {
38  e.preventDefault();
39
40  const form = e.target;
41  const formData = new FormData(form);
42
43  try {
44    const response = await fetch('https://api.jurassic-park.com/dinosaurs/import', {
45      method: 'POST',
46      body: formData // FormData jest automatycznie ustawiane jako 'multipart/form-data'
47    });
48
49    if (!response.ok) {
50      throw new Error(`Błąd HTTP: ${response.status}`);
51    }
52
53    const result = await response.json();
54    alert(`Import zakończony! Dodano ${result.count} dinozaurów.`);
55  } catch (error) {
56    console.error('Błąd podczas importu:', error);
57    alert(`Błąd: ${error.message}`);
58  }
59});

Obsługa CORS (Cross-Origin Resource Sharing)

CORS to mechanizm bezpieczeństwa, który kontroluje, czy strona może wykonywać żądania do zasobów na innej domenie. Domyślnie, żądania między domenami są zabronione przez politykę "same-origin" przeglądarek.

Podstawowe żądanie CORS:

1fetch('https://api.different-domain.com/data', {
2  mode: 'cors' // Domyślna wartość
3})

Żądanie z uwierzytelnianiem:

1fetch('https://api.different-domain.com/protected-data', {
2  mode: 'cors',
3  credentials: 'include' // Wysyła ciasteczka cross-origin
4})

Opcje dla parametru

credentials
:

  • omit
    : Nie wysyłaj ciasteczek (domyślnie)
  • same-origin
    : Wysyłaj ciasteczka tylko do tej samej domeny
  • include
    : Wysyłaj ciasteczka również do innych domen

Obsługa cachowania

Fetch pozwala na kontrolowanie, jak przeglądarka używa swojego cache:

1// Ignorowanie cache, zawsze wykonaj żądanie do serwera
2fetch(url, {
3  cache: 'no-store'
4})
5
6// Najpierw sprawdź cache, jeśli nie ma tam odpowiedzi, wykonaj żądanie
7fetch(url, {
8  cache: 'default'
9})
10
11// Najpierw sprawdź cache, ale zweryfikuj z serwerem, czy odpowiedź jest aktualna
12fetch(url, {
13  cache: 'no-cache'
14})
15
16// Używaj tylko cache, nie wykonuj żądania sieciowego
17fetch(url, {
18  cache: 'only-if-cached',
19  mode: 'same-origin' // Wymagane dla 'only-if-cached'
20})

Praktyczne wzorce z Fetch API

1. Funkcja uniwersalna do żądań API

1// Uniwersalna funkcja do wykonywania żądań API
2async function apiRequest(endpoint, options = {}) {
3  const apiBaseUrl = 'https://api.jurassic-park.com';
4  const url = `${apiBaseUrl}${endpoint}`;
5
6  // Domyślne opcje
7  const defaultOptions = {
8    headers: {
9      'Content-Type': 'application/json',
10      'Authorization': `Bearer ${localStorage.getItem('apiToken')}`
11    },
12    mode: 'cors'
13  };
14
15  // Łączenie opcji, z priorytetem dla tych przekazanych
16  const mergedOptions = {
17    ...defaultOptions,
18    ...options,
19    headers: {
20      ...defaultOptions.headers,
21      ...(options.headers || {})
22    }
23  };
24
25  // Automatyczne przekształcanie body do JSON, jeśli to obiekt
26  if (mergedOptions.body && typeof mergedOptions.body === 'object' && !(mergedOptions.body instanceof FormData)) {
27    mergedOptions.body = JSON.stringify(mergedOptions.body);
28  }
29
30  try {
31    const response = await fetch(url, mergedOptions);
32
33    // Obsługa różnych kodów statusu
34    if (response.status === 401) {
35      // Odświeżanie tokenu lub przekierowanie do logowania
36      await refreshAuthToken();
37      // Ponowienie żądania z nowym tokenem
38      return apiRequest(endpoint, options);
39    }
40
41    if (response.status === 204) { // No Content
42      return { success: true };
43    }
44
45    if (!response.ok) {
46      // Próba pobrania błędu w formacie JSON
47      try {
48        const errorData = await response.json();
49        throw new Error(errorData.message || `Błąd HTTP: ${response.status}`);
50      } catch (e) {
51        // Jeśli nie możemy sparsować błędu jako JSON, użyj prostego komunikatu
52        throw new Error(`Błąd HTTP: ${response.status}`);
53      }
54    }
55
56    // Sprawdź Content-Type, aby określić format odpowiedzi
57    const contentType = response.headers.get('content-type');
58    if (contentType && contentType.includes('application/json')) {
59      return await response.json();
60    } else if (contentType && contentType.includes('text/')) {
61      return await response.text();
62    } else {
63      return await response.blob();
64    }
65  } catch (error) {
66    // Obsługa błędów sieci i innych
67    console.error(`Błąd żądania API (${endpoint}):`, error);
68
69    // Niestandardowy obiekt błędu z dodatkowymi informacjami
70    const apiError = new Error(error.message);
71    apiError.isNetworkError = !window.navigator.onLine;
72    apiError.endpoint = endpoint;
73    apiError.originalError = error;
74
75    throw apiError;
76  }
77}
78
79// Przykład użycia
80async function pobierzDinozaury() {
81  const dinozaury = await apiRequest('/dinosaurs?limit=50');
82  return dinozaury;
83}
84
85async function aktualizujDinozaura(id, dane) {
86  return apiRequest(`/dinosaurs/${id}`, {
87    method: 'PUT',
88    body: dane
89  });
90}
91
92async function importujDaneZPliku(plik) {
93  const formData = new FormData();
94  formData.append('file', plik);
95
96  return apiRequest('/import', {
97    method: 'POST',
98    body: formData,
99    headers: {
100      // Usunięcie Content-Type, aby przeglądarka mogła ustawić prawidłowy z boundary
101      'Content-Type': undefined
102    }
103  });
104}

2. Wzorzec retry - automatyczne ponowne próby po błędzie

1/**
2 * Wykonuje funkcję z automatycznymi ponownymi próbami
3 * @param {Function} fn - Funkcja do wykonania, która zwraca Promise
4 * @param {Object} options - Opcje
5 * @param {number} options.maxRetries - Maksymalna liczba ponownych prób
6 * @param {number} options.retryDelay - Opóźnienie przed ponowną próbą (ms)
7 * @param {Function} options.shouldRetry - Funkcja określająca, czy ponawiać próbę
8 * @param {number} options.backoffFactor - Mnożnik opóźnienia (backoff)
9 * @returns {Promise} - Promise z wynikiem funkcji lub ostatnim błędem
10 */
11async function withRetry(fn, options = {}) {
12  const {
13    maxRetries = 3,
14    retryDelay = 1000,
15    shouldRetry = (error) => true,
16    backoffFactor = 2
17  } = options;
18
19  let lastError;
20
21  for (let attempt = 0; attempt <= maxRetries; attempt++) {
22    try {
23      return await fn();
24    } catch (error) {
25      lastError = error;
26
27      if (attempt === maxRetries || !shouldRetry(error)) {
28        break;
29      }
30
31      const delay = retryDelay * Math.pow(backoffFactor, attempt);
32      console.log(`Próba ${attempt + 1} nie powiodła się. Ponowienie za ${delay}ms...`);
33      await new Promise(resolve => setTimeout(resolve, delay));
34    }
35  }
36
37  throw lastError;
38}
39
40// Przykład użycia z Fetch API
41async function pobierzDaneZChwilowegoAPI() {
42  return withRetry(
43    () => fetch('https://api.jurassic-park.com/unstable-endpoint')
44      .then(response => {
45        if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
46        return response.json();
47      }),
48    {
49      maxRetries: 5,
50      retryDelay: 500,
51      backoffFactor: 1.5,
52      shouldRetry: (error) => {
53        // Ponawiaj próbę tylko dla błędów 5xx (błędy serwera) lub błędów sieci
54        return error.message.includes('HTTP Error: 5') ||
55               error instanceof TypeError;
56      }
57    }
58  );
59}

3. Prosty system cache dla żądań

1// Prosty system cache dla żądań GET
2const apiCache = {
3  cache: new Map(),
4  ttl: 60000, // czas życia cache w ms (1 minuta)
5
6  async get(url, options = {}) {
7    const cacheKey = `${url}|${JSON.stringify(options)}`;
8    const cachedItem = this.cache.get(cacheKey);
9
10    // Jeśli mamy ważny cached item, użyj go
11    if (cachedItem && Date.now() < cachedItem.expiry) {
12      console.log(`Używam cache dla: ${url}`);
13      return cachedItem.data;
14    }
15
16    // W przeciwnym razie wykonaj żądanie
17    console.log(`Wykonuję żądanie dla: ${url}`);
18    const response = await fetch(url, options);
19
20    if (!response.ok) {
21      throw new Error(`Błąd HTTP: ${response.status}`);
22    }
23
24    const data = await response.json();
25
26    // Zapisz w cache, jeśli to żądanie GET
27    if (!options.method || options.method === 'GET') {
28      this.cache.set(cacheKey, {
29        data,
30        expiry: Date.now() + this.ttl
31      });
32    }
33
34    return data;
35  },
36
37  invalidate(urlPattern) {
38    // Usuń z cache wszystkie klucze pasujące do wzorca
39    for (const key of this.cache.keys()) {
40      if (key.includes(urlPattern)) {
41        this.cache.delete(key);
42      }
43    }
44  },
45
46  clear() {
47    this.cache.clear();
48  }
49};
50
51// Przykład użycia
52async function pobierzDaneDinozaura(id) {
53  return apiCache.get(`https://api.jurassic-park.com/dinosaurs/${id}`);
54}
55
56async function zaktualizujDinozaura(id, dane) {
57  const result = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, {
58    method: 'PUT',
59    headers: { 'Content-Type': 'application/json' },
60    body: JSON.stringify(dane)
61  }).then(r => r.json());
62
63  // Unieważnij cache dla tego dinozaura i list dinozaurów
64  apiCache.invalidate(`/dinosaurs/${id}`);
65  apiCache.invalidate('/dinosaurs?');
66
67  return result;
68}

Ograniczenia i kompatybilność Fetch API

Fetch API jest dostępny we wszystkich nowoczesnych przeglądarkach, a od wersji 18 również natywnie w Node.js. Jednak istnieje kilka ograniczeń i problemów z kompatybilnością:

  1. Nie obsługuje postępu żądania - Fetch nie ma wbudowanej obsługi postępu ładowania/wysyłania. Dla takich przypadków czasem lepiej jest użyć XMLHttpRequest.

  2. Nie posiada natywnego timeoutu - Timeout trzeba implementować za pomocą AbortController.

  3. Nie odrzuca Promise dla błędów HTTP - Nawet jeśli serwer zwróci kod błędu, Fetch Promise zostanie rozwiązany. Trzeba sprawdzać response.ok.

W starszych przeglądarkach może być konieczne użycie polyfilla, takiego jak

whatwg-fetch
lub biblioteki takiej jak
axios
, która zapewnia jednolite API na różnych środowiskach.

Alternatywy dla Fetch API

Choć Fetch API jest dostępna natywnie, istnieją alternatywy, które mogą zaoferować dodatkowe funkcje:

  1. Axios - Popularna biblioteka, która zapewnia:

    • Jednolite API między przeglądarką a Node.js
    • Automatyczną transformację danych JSON
    • Wbudowaną ochronę przed XSRF
    • Obsługę postępu żądania
    • Wbudowane timeouty
    • Automatyczne ponowne próby
    • Anulowanie żądań
  2. jQuery.ajax() - W starszych projektach używających jQuery

  3. XMLHttpRequest - Starsza, bardziej szczegółowa alternatywa, która oferuje więcej kontroli, np. nad postępem żądań

Kiedy używać Fetch API, a kiedy alternatyw?

  • Fetch jest idealny, gdy:

    • Tworzysz nowy projekt
    • Chcesz minimalizować zależności
    • Potrzebujesz prostych żądań HTTP
    • Pracujesz z nowoczesnymi przeglądarkami
  • Rozważ alternatywy, gdy:

    • Potrzebujesz bardziej zaawansowanych funkcji (postęp, wbudowane ponowne próby)
    • Musisz wspierać starsze przeglądarki bez polyfillów
    • Pracujesz w środowisku, gdzie Fetch nie działa dobrze
    • Już używasz biblioteki, która oferuje funkcje HTTP

Dobre praktyki przy pracy z Fetch

  1. Zawsze sprawdzaj response.ok

    1fetch(url).then(response => {
    2  if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
    3  return response.json();
    4});
  2. Ustaw odpowiednie nagłówki

    1fetch(url, {
    2  headers: {
    3    'Content-Type': 'application/json',
    4    'Accept': 'application/json'
    5  }
    6});
  3. Używaj AbortController dla timeoutów

    1const controller = new AbortController();
    2const timeout = setTimeout(() => controller.abort(), 5000);
    3
    4fetch(url, { signal: controller.signal })
    5  .then(response => /* ... */)
    6  .catch(error => /* ... */)
    7  .finally(() => clearTimeout(timeout));
  4. Ostrożnie z przetwarzaniem body

    1// ŹLE - próba wielokrotnego użycia body
    2fetch(url)
    3  .then(response => {
    4    const text = response.text(); // To konsumuje body!
    5    const json = response.json(); // Błąd - body już zostało zużyte!
    6    return /* ... */;
    7  });
    8
    9// DOBRZE - sklonuj response, jeśli potrzebujesz go użyć wielokrotnie
    10fetch(url)
    11  .then(response => {
    12    const clone = response.clone();
    13    return response.text().then(text => {
    14      return clone.json().then(json => {
    15        return { text, json };
    16      });
    17    });
    18  });
  5. Używaj async/await dla czytelności

    1// Zamiast łańcuchowania .then()
    2async function fetchData() {
    3  try {
    4    const response = await fetch(url);
    5    if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
    6    const data = await response.json();
    7    return data;
    8  } catch (error) {
    9    console.error('Błąd:', error);
    10    throw error;
    11  }
    12}

Podsumowanie

Fetch API jest potężnym, wbudowanym narzędziem do wykonywania żądań HTTP w JavaScript. Jego główne zalety to Promise-based API, prostota użycia i wszechstronność. Chociaż ma pewne ograniczenia w porównaniu do bardziej zaawansowanych bibliotek, jak Axios, dla większości przypadków użycia jest całkowicie wystarczający.

W Parku Jurajskim, gdzie stabilna komunikacja między różnymi systemami jest kluczowa dla bezpieczeństwa odwiedzających i dinozaurów, Fetch API pozwala na niezawodną wymianę danych między systemami monitorującymi, bazami danych i interfejsami użytkownika.

W następnym ćwiczeniu przyjrzymy się, jak łączyć Fetch API z innymi technikami asynchronicznymi, aby tworzyć jeszcze bardziej zaawansowane systemy do zarządzania naszym cyfrowym Parkiem Jurajskim.

Przejdź do CodeWorlds