Kurs JavaScript i TypeScript · Moduł 4: Programowanie asynchroniczne
API Fetch
W tej lekcji18
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 domenyinclude: 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ą:
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.
Nie posiada natywnego timeoutu - Timeout trzeba implementować za pomocą AbortController.
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:
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ń
jQuery.ajax() - W starszych projektach używających jQuery
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
Zawsze sprawdzaj response.ok
1fetch(url).then(response => { 2 if (!response.ok) throw new Error(`HTTP Error: ${response.status}`); 3 return response.json(); 4});Ustaw odpowiednie nagłówki
1fetch(url, { 2 headers: { 3 'Content-Type': 'application/json', 4 'Accept': 'application/json' 5 } 6});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));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 });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.
Kod do tej lekcji: index.js
1// Callback Function Builder - Funkcje zwrotne
2console.log("CALLBACK FUNCTIONS - SYSTEM ZDARZEŃ PARKU\n");
3
4// Callback - funkcja przekazana jako argument
5function monitorDinosaur(species, callback) {
6 console.log(`Rozpoczynam monitoring: ${species}`);
7
8 // Symulacja monitoringu (synchroniczny dla uproszczenia)
9 const status = Math.random() > 0.5 ? 'SAFE' : 'ALERT';
10 const data = {
11 species: species,
12 status: status,
13 timestamp: new Date().toLocaleTimeString()
14 };
15
16 // Wywołanie callback z wynikami
17 callback(data);
18}
19
20// Przykład 1: Prosty callback
21console.log("=== PRZYKŁAD 1: PROSTY CALLBACK ===\n");
22
23monitorDinosaur('T-Rex', function(result) {
24 console.log(`Wynik monitoringu:`);
25 console.log(` Gatunek: ${result.species}`);
26 console.log(` Status: ${result.status}`);
27 console.log(` Czas: ${result.timestamp}`);
28});
29
30// Przykład 2: Arrow function jako callback
31console.log("\n=== PRZYKŁAD 2: ARROW FUNCTION ===\n");
32
33monitorDinosaur('Velociraptor', (result) => {
34 const emoji = result.status === 'SAFE' ? 'OK' : '!';
35 console.log(`${emoji} ${result.species}: ${result.status} (${result.timestamp})`);
36});
37
38// Przykład 3: Różne callbacki dla różnych sytuacji
39console.log("\n=== PRZYKŁAD 3: WARUNKOWE CALLBACKI ===\n");
40
41function checkDinosaurWithCallbacks(species, onSafe, onAlert) {
42 const isSafe = Math.random() > 0.5;
43
44 if (isSafe) {
45 onSafe(species);
46 } else {
47 onAlert(species);
48 }
49}
50
51checkDinosaurWithCallbacks(
52 'Triceratops',
53 (species) => console.log(`${species} jest bezpieczny`),
54 (species) => console.log(`ALERT! Problem z ${species}!`)
55);
56
57// Przykład 4: Callback z array methods
58console.log("\n=== PRZYKŁAD 4: ARRAY METHODS Z CALLBACKS ===\n");
59
60const dinosaurs = [
61 { name: 'T-Rex', danger: 10 },
62 { name: 'Velociraptor', danger: 8 },
63 { name: 'Triceratops', danger: 3 },
64 { name: 'Brachiosaurus', danger: 2 }
65];
66
67console.log("forEach - iteracja:");
68dinosaurs.forEach((dino) => {
69 console.log(` - ${dino.name} (zagrożenie: ${dino.danger})`);
70});
71
72console.log("\nfilter - tylko niebezpieczne (>5):");
73const dangerous = dinosaurs.filter((dino) => dino.danger > 5);
74dangerous.forEach(dino => console.log(` ${dino.name}`));
75
76console.log("\nmap - nazwy:");
77const names = dinosaurs.map((dino) => dino.name);
78console.log(` ${names.join(', ')}`);
79
80console.log("\nreduce - suma zagrożenia:");
81const totalDanger = dinosaurs.reduce((sum, dino) => sum + dino.danger, 0);
82console.log(` Łącznie: ${totalDanger}`);
83
84// Przykład 5: Callback hell (problem!)
85console.log("\n=== PRZYKŁAD 5: CALLBACK HELL ===\n");
86
87function step1(callback) {
88 console.log("Krok 1: Sprawdzanie ogrodzenia...");
89 callback();
90}
91
92function step2(callback) {
93 console.log("Krok 2: Sprawdzanie dinozaurów...");
94 callback();
95}
96
97function step3(callback) {
98 console.log("Krok 3: Raport końcowy...");
99 callback();
100}
101
102// Zagnieżdżone callbacki (callback hell)
103step1(() => {
104 step2(() => {
105 step3(() => {
106 console.log("Wszystkie kroki zakończone");
107 });
108 });
109});
110
111console.log("\nProblem: Callback hell - trudny do czytania!");
112console.log("Rozwiązanie: Promises lub async/await (w następnych lekcjach)");Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co zwraca funkcja fetch() w JavaScript?
2. Do czego służy AbortController w kontekście Fetch API?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Napisz async function, która pobiera dane z API za pomocą fetch(), parsuje odpowiedź JSON i wyświetla listę dinozaurów.
- Klikanie w kolejności
Ułóż elementy łańcucha Promise:
- Edytor kodu
Napisz kod używający Promise.allSettled do równoległego sprawdzenia kilku czujników. Przeanalizuj wyniki - które się udały (fulfilled), a które nie (rejected).
- Układanie w pionie
Ułóż elementy obsługi błędów w async/await: