Przejdź do treści
School IT / Przestrzeń do nauki

HttpClient — pobieranie danych z serwera

Do tej pory dane były wpisane w kodzie. W prawdziwej aplikacji przychodzą z serwera — z opóźnieniem, a czasem wcale. Pobierzesz listę wydarzeń z pliku JSON, obsłużysz ładowanie i błąd, a na koniec przeniesiesz pobieranie do serwisu.

Angular 22 HttpClient get subscribe JSON ładowanie i błąd serwis z danymi 50 min
CEL LEKCJI

Czego się dziś nauczysz

  • Przygotujesz plik JSON w folderze public i otworzysz go w przeglądarce.
  • Pobierzesz tablicę obiektów przez HttpClient i zapiszesz ją w sygnale.
  • Obsłużysz trzy stany: ładowanie, dane i błąd — sygnałami i blokami @if.
  • Podejrzysz zapytanie w zakładce Network narzędzi programisty.
  • Przeniesiesz pobieranie do serwisu, z którego korzysta kilka komponentów.

Przygotowanie: lekcja 07 (folder public), lekcje 09–13 (sygnały i bloki sterujące), lekcja 30 (interfejs), lekcja 33 (serwis i inject). Przewidywany czas: 50 min z zadaniami.

TEORIA

Dane przychodzą z opóźnieniem

Pobranie danych z sieci trwa — czasem 50 milisekund, czasem 5 sekund, a czasem kończy się błędem. Program nie może na to czekać, zatrzymując całą stronę. Dlatego HttpClient działa tak: wysyłasz zapytanie, podajesz funkcję „co zrobić, gdy przyjdzie odpowiedź” i program idzie dalej.

KrokKodUwagi
1. Plik z danymipublic/dane/wydarzenia.jsonw przeglądarce pod adresem /dane/wydarzenia.json — jak obrazki z lekcji 07
2. HttpClient w klasieprivate readonly http = inject(HttpClient);import z @angular/common/http
3. Zapytaniethis.http.get<Wydarzenie[]>('/dane/wydarzenia.json')typ mówi, czego się spodziewamy
4. Odbiór odpowiedzi.subscribe(dane => this.wydarzenia.set(dane))bez subscribe zapytanie nie zostanie wysłane

Konfiguracja w Angularze 22

W Angularze 22 HttpClient jest dostępny od razu — nie trzeba niczego dopisywać w app.config.ts. W starszych projektach i poradnikach zobaczysz w providers wywołanie provideHttpClient(); dziś dopisuje się je tylko wtedy, gdy trzeba zmienić ustawienia klienta HTTP.

public/dane/wydarzenia.json — pełny plik
[
  { "id": 1, "tytul": "Turniej retro", "sala": "12", "miejsca": 16 },
  { "id": 2, "tytul": "Noc robotów", "sala": "7", "miejsca": 0 },
  { "id": 3, "tytul": "Warsztat druku 3D", "sala": "14", "miejsca": 3 },
  { "id": 4, "tytul": "Liga planszówek", "sala": "Biblioteka", "miejsca": 10 }
]

JSON wygląda prawie jak tablica obiektów w TypeScripcie, ale ma ostrzejsze reguły: nazwy pól i teksty w cudzysłowach (nie apostrofach), bez przecinka po ostatnim elemencie, bez komentarzy. Po zapisaniu pliku otwórz w przeglądarce localhost:4200/dane/wydarzenia.json — jeśli widzisz dane, adres jest dobry.

PRZYKŁAD 1

Pierwsze pobranie danych

Interfejs opisuje jeden obiekt z pliku JSON. Komponent przy starcie pobiera listę i wyświetla ją w tabeli.

src/app/nowa-36-1/wydarzenie.ts — pełny plik
export interface Wydarzenie {
    id: number;
    tytul: string;
    sala: string;
    miejsca: number;
}
src/app/nowa-36-1/demo.ts — pełny plik
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Wydarzenie } from './wydarzenie';

@Component({
    selector: 'app-demo',
    imports: [],
    templateUrl: './demo.html'
})
export class Demo {
    private readonly http = inject(HttpClient);
    readonly wydarzenia = signal<Wydarzenie[]>([]);

    constructor() {
        this.http.get<Wydarzenie[]>('/dane/wydarzenia.json').subscribe(dane => {
            this.wydarzenia.set(dane);
        });
    }
}
src/app/nowa-36-1/demo.html — pełny plik
<div class="p-3">
  <table class="table" style="max-width: 36rem">
    <thead><tr><th>Wydarzenie</th><th>Sala</th><th>Wolne miejsca</th></tr></thead>
    <tbody>
      @for (w of wydarzenia(); track w.id) {
        <tr><td>{{ w.tytul }}</td><td>{{ w.sala }}</td><td>{{ w.miejsca }}</td></tr>
      }
    </tbody>
  </table>
</div>
wynik
Wydarzenie          Sala         Wolne miejsca
Turniej retro       12           16
Noc robotów         7            0
Warsztat druku 3D   14           3
Liga planszówek     Biblioteka   10

constructor() to metoda wywoływana raz, przy tworzeniu komponentu — dobre miejsce na start pobierania. subscribe(dane => …) przyjmuje funkcję, którą Angular wywoła później, gdy odpowiedź dotrze. Zmień w pliku JSON dowolny tytuł i odśwież stronę — zmiana pojawi się bez zmieniania kodu.

Zakładka Network

F12 → Network (Sieć) i odśwież stronę: na liście jest zapytanie wydarzenia.json ze statusem 200. Kliknij je i zajrzyj w zakładkę Response — to dokładnie zawartość pliku. Tak sprawdzisz każde zapytanie, które „nie działa”.

Zatrzymaj się

Dopisz pod tabelą {{ wydarzenia().length }} i odśwież stronę kilka razy. Czy przez chwilę widać 0? Dlaczego ta chwila może być bardzo krótka albo bardzo długa?

PRZYKŁAD 2

Ładowanie, dane, błąd

Ten sam komponent z pełną obsługą stanów. subscribe dostaje obiekt z dwiema funkcjami: next (przyszły dane) i error (coś się nie udało). Dwa przyciski pobierają dane z dobrego i złego adresu, żeby zobaczyć oba zakończenia.

src/app/nowa-36-2/demo.ts — pełny plik
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Wydarzenie } from './wydarzenie';

@Component({
    selector: 'app-demo',
    imports: [],
    templateUrl: './demo.html'
})
export class Demo {
    private readonly http = inject(HttpClient);

    readonly ladowanie = signal(false);
    readonly blad = signal('');
    readonly wydarzenia = signal<Wydarzenie[]>([]);

    pobierz(adres: string): void {
        this.ladowanie.set(true);
        this.blad.set('');
        this.wydarzenia.set([]);

        this.http.get<Wydarzenie[]>(adres).subscribe({
            next: dane => {
                this.wydarzenia.set(dane);
                this.ladowanie.set(false);
            },
            error: () => {
                this.blad.set('Nie udało się pobrać danych z adresu ' + adres);
                this.ladowanie.set(false);
            }
        });
    }
}
src/app/nowa-36-2/demo.html — pełny plik
<div class="p-3">
  <div style="max-width: 36rem">
    <button class="btn btn-primary me-2" (click)="pobierz('/dane/wydarzenia.json')">Pobierz</button>
    <button class="btn btn-outline-danger" (click)="pobierz('/dane/brak-takiego-pliku.json')">Pobierz ze złego adresu</button>

    <div class="mt-3">
      @if (ladowanie()) {
        <div class="spinner-border spinner-border-sm"></div> Ładowanie…
      } @else if (blad()) {
        <div class="alert alert-danger">{{ blad() }}</div>
      } @else {
        <ul class="list-group">
          @for (w of wydarzenia(); track w.id) {
            <li class="list-group-item">{{ w.tytul }} — sala {{ w.sala }}</li>
          } @empty {
            <li class="list-group-item text-secondary">Kliknij „Pobierz”.</li>
          }
        </ul>
      }
    </div>
  </div>
</div>
po kliknięciu „Pobierz ze złego adresu”
[Pobierz] [Pobierz ze złego adresu]
Nie udało się pobrać danych z adresu /dane/brak-takiego-pliku.json   (czerwony alert)

Plik wydarzenie.ts jest taki jak w przykładzie 1. Trzy sygnały opisują stan, a łańcuch @if … @else if … @else pokazuje dokładnie jeden z nich. W zakładce Network złe zapytanie ma status 404. Na szybkim komputerze „Ładowanie…” mignie tak krótko, że go nie zobaczysz — tym zajmuje się przykład 3.

Zatrzymaj się

W obu funkcjach — next i error — jest this.ladowanie.set(false). Co by się stało, gdybyś zapomniał o nim w error?

PRZYKŁAD 3

Symulacja wolnego połączenia

Żeby zobaczyć stan ładowania, spowalniamy odpowiedź operatorem delay z biblioteki RxJS, na której opiera się HttpClient. .pipe(delay(1500)) wstrzymuje odpowiedź o półtorej sekundy. Przycisk jest zablokowany w trakcie ładowania, żeby nie wysyłać zapytań seriami.

src/app/nowa-36-3/demo.ts — pełny plik
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { delay } from 'rxjs';
import { Wydarzenie } from './wydarzenie';

@Component({
    selector: 'app-demo',
    imports: [],
    templateUrl: './demo.html'
})
export class Demo {
    private readonly http = inject(HttpClient);

    readonly ladowanie = signal(false);
    readonly blad = signal('');
    readonly wydarzenia = signal<Wydarzenie[]>([]);
    readonly ostatnio = signal<Date | null>(null);

    constructor() {
        this.odswiez();
    }

    odswiez(): void {
        this.ladowanie.set(true);
        this.blad.set('');
        this.http.get<Wydarzenie[]>('/dane/wydarzenia.json')
            .pipe(delay(1500))
            .subscribe({
                next: dane => {
                    this.wydarzenia.set(dane);
                    this.ostatnio.set(new Date());
                    this.ladowanie.set(false);
                },
                error: () => {
                    this.blad.set('Brak połączenia z serwerem.');
                    this.ladowanie.set(false);
                }
            });
    }
}
src/app/nowa-36-3/demo.html — pełny plik
<div class="card m-3" style="max-width: 32rem">
  <div class="card-header d-flex justify-content-between align-items-center">
    Wydarzenia
    <button class="btn btn-sm btn-outline-primary" [disabled]="ladowanie()" (click)="odswiez()">
      @if (ladowanie()) { <span class="spinner-border spinner-border-sm"></span> } Odśwież
    </button>
  </div>
  <ul class="list-group list-group-flush" [class.opacity-50]="ladowanie()">
    @for (w of wydarzenia(); track w.id) {
      <li class="list-group-item">{{ w.tytul }}</li>
    }
  </ul>
  @if (blad()) { <div class="alert alert-danger m-2">{{ blad() }}</div> }
  @if (ostatnio(); as o) {
    <div class="card-footer small text-secondary">Odświeżono o {{ o.toLocaleTimeString() }}</div>
  }
</div>
pierwsze 1,5 s → po załadowaniu
Wydarzenia                     [◌ Odśwież] (zablokowany)
──────────────────────────────────────────
Wydarzenia                     [Odśwież]
Turniej retro
Noc robotów
Warsztat druku 3D
Liga planszówek
Odświeżono o 18:42:07

signal<Date | null>(null) to sygnał, który może być datą albo null — na starcie nie ma jeszcze daty odświeżenia. „Odśwież” wygasza listę (opacity-50) na czas ładowania, ale jej nie czyści — użytkownik widzi stare dane, dopóki nie przyjdą nowe. Wolne łącze możesz też ustawić bez zmiany kodu: F12 → Network → No throttling → Slow 4G. Przed oddaniem projektu usuń delay.

Zatrzymaj się

W przykładzie 2 lista była czyszczona przed pobraniem, tu nie. Które zachowanie jest wygodniejsze dla użytkownika i kiedy?

PRZYKŁAD 4

Pobieranie w serwisie

Dane z serwera zwykle potrzebuje kilka komponentów. Pobieranie przenosimy do serwisu WydarzeniaSerwis (lekcja 33): ma sygnały stanu i metodę wczytaj, która nie pobiera drugi raz, jeśli dane już są albo właśnie się pobierają. Licznik i lista tylko z niego czytają.

src/app/nowa-36-4/wydarzenia-serwis.ts — pełny plik
import { Service, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Wydarzenie } from './wydarzenie';

@Service()
export class WydarzeniaSerwis {
    private readonly http = inject(HttpClient);

    readonly lista = signal<Wydarzenie[]>([]);
    readonly ladowanie = signal(false);
    readonly blad = signal('');
    readonly wolneRazem = computed(() => {
        let suma = 0;
        for (const w of this.lista()) {
            suma += w.miejsca;
        }
        return suma;
    });

    wczytaj(): void {
        // dane już są albo właśnie się pobierają — nie wysyłamy drugiego zapytania
        if (this.lista().length > 0 || this.ladowanie()) {
            return;
        }
        this.ladowanie.set(true);
        this.http.get<Wydarzenie[]>('/dane/wydarzenia.json').subscribe({
            next: dane => {
                this.lista.set(dane);
                this.ladowanie.set(false);
            },
            error: () => {
                this.blad.set('Nie udało się pobrać wydarzeń.');
                this.ladowanie.set(false);
            }
        });
    }
}
src/app/nowa-36-4/licznik.ts — pełny plik
import { Component, inject } from '@angular/core';
import { WydarzeniaSerwis } from './wydarzenia-serwis';

@Component({
    selector: 'app-licznik',
    imports: [],
    template: `
      <div class="alert alert-info">
        Wydarzeń: {{ dane.lista().length }}, wolnych miejsc łącznie: {{ dane.wolneRazem() }}
      </div>
    `
})
export class Licznik {
    readonly dane = inject(WydarzeniaSerwis);
}
src/app/nowa-36-4/lista.ts — pełny plik
import { Component, inject } from '@angular/core';
import { WydarzeniaSerwis } from './wydarzenia-serwis';

@Component({
    selector: 'app-lista',
    imports: [],
    template: `
      @if (dane.ladowanie()) {
        <p>Ładowanie…</p>
      } @else if (dane.blad()) {
        <div class="alert alert-danger">{{ dane.blad() }}</div>
      } @else {
        <ul class="list-group">
          @for (w of dane.lista(); track w.id) {
            <li class="list-group-item d-flex justify-content-between">
              {{ w.tytul }}
              <span class="badge" [class.text-bg-success]="w.miejsca > 0" [class.text-bg-danger]="w.miejsca === 0">{{ w.miejsca }}</span>
            </li>
          }
        </ul>
      }
    `
})
export class Lista {
    readonly dane = inject(WydarzeniaSerwis);

    constructor() {
        this.dane.wczytaj();
    }
}
src/app/nowa-36-4/demo.ts — pełny plik
import { Component } from '@angular/core';
import { Licznik } from './licznik';
import { Lista } from './lista';

@Component({
    selector: 'app-demo',
    imports: [Licznik, Lista],
    templateUrl: './demo.html'
})
export class Demo {}
src/app/nowa-36-4/demo.html — pełny plik
<div class="p-3">
  <div style="max-width: 34rem">
    <app-licznik />
    <app-lista />
  </div>
</div>
wynik
Wydarzeń: 4, wolnych miejsc łącznie: 29
Turniej retro       [16]
Noc robotów         [0]
Warsztat druku 3D   [3]
Liga planszówek     [10]

W zakładce Network jest jedno zapytanie, choć z danych korzystają dwa komponenty. Gdyby trzeci i czwarty komponent też wywoływał wczytaj(), zapytanie i tak byłoby jedno — serwis pamięta, że dane już ma albo właśnie je pobiera.

httpResource

W nowszym kodzie zobaczysz też httpResource(() => '/dane/wydarzenia.json') — funkcję, która sama tworzy sygnały wartości, ładowania i błędu. Działa na tej samej zasadzie co nasze trzy sygnały; warto ją poznać, gdy ręczna wersja jest już jasna.

Zatrzymaj się

Licznik nie wywołuje wczytaj(), a mimo to pokazuje dane. Co by pokazał, gdyby na stronie był sam licznik, bez listy?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenie
HttpClient z @angular/common/httpklient HTTP; pobierany przez inject
http.get<Typ>(adres)przygotowuje zapytanie GET (Observable — „obietnica” danych)
.subscribe(dane => …)wysyła zapytanie i odbiera dane
.subscribe({ next: …, error: … })osobna obsługa sukcesu i błędu
.pipe(delay(1500)), delay z rxjssztuczne opóźnienie odpowiedzi
constructor() { … }kod uruchamiany raz przy tworzeniu komponentu lub serwisu
signal<Date | null>(null)sygnał, który może nie mieć jeszcze wartości
new Date().toLocaleTimeString()bieżąca godzina jako tekst
spinner-border spinner-border-smwskaźnik ładowania Bootstrapa
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
this.http.get(…) bez subscribeNic się nie dzieje — w zakładce Network nie ma żadnego zapytania. Zapytanie wysyła dopiero subscribe.
const dane = this.http.get(…); this.lista.set(dane);TS2345: Argument of type 'Observable<…>’ is not assignable to parameter of type '…[]’. get nie zwraca danych, tylko „obietnicę”; dane przychodzą w funkcji z subscribe.
Adres public/dane/… albo dane/… bez ukośnikaBłąd 404 albo adres liczony względem bieżącej strony. Folder public nie występuje w adresie; zaczynaj od ukośnika: /dane/wydarzenia.json.
Przecinek po ostatnim elemencie, apostrofy w JSONStatus 200, a mimo to wywołuje się error: Http failure during parsing. Plik nie jest poprawnym JSON-em.
Brak funkcji errorPrzy błędzie użytkownik widzi wieczne „Ładowanie…”. Zawsze obsłuż oba zakończenia.
Pola interfejsu inne niż w pliku JSON (nazwa zamiast tytul)Brak błędu kompilacji — TypeScript wierzy typowi z get<…>. Na stronie puste komórki. Sprawdź nazwy w Response.
ZADANIA

Zadania

ZAD 1Twój plik JSON★☆☆

Utwórz public/dane/uczniowie.json z tablicą pięciu uczniów (imie, klasa, srednia) i wyświetl ich w tabeli.

Kryterium sukcesu: plik otwiera się w przeglądarce pod /dane/uczniowie.json; zmiana w pliku po odświeżeniu strony zmienia tabelę.

Podpowiedź

Przykład 1 z innym interfejsem i adresem.

ZAD 2Menu stołówki★☆☆

Plik menu.json z daniami (nazwa, cena, wege — true/false). Wyświetl je jako listę z ceną w złotych; dania wegetariańskie mają zieloną etykietę „wege”.

Kryterium sukcesu: lista zgadza się z plikiem; etykieta tylko przy daniach z "wege": true.

Podpowiedź

W JSON wartości logiczne piszesz bez cudzysłowów: true, false. Cena: pipe currency z lekcji 16 albo toFixed(2).

ZAD 3Plan lekcji z serwera★★☆

Plik plan.json z planem na pięć dni (tablica obiektów: dzien i tablica przedmiotów). Wyświetl go jako karty, z obsługą stanu ładowania i błędu oraz przyciskiem „Odśwież”.

Kryterium sukcesu: widoczne są wszystkie trzy stany (ładowanie z delay, dane, błąd po zmianie adresu na zły).

Podpowiedź

Przykład 3. Dla zagnieżdżonych tablic potrzebujesz dwóch pętli @for, jednej w drugiej.

ZAD 4Wyszukiwarka w pobranych danych★★☆

Pobierz wydarzenia.json, a nad listą dodaj pole wyszukiwania z [(ngModel)] i checkbox „tylko z wolnymi miejscami”. Lista pokazuje tylko pasujące wydarzenia.

Kryterium sukcesu: filtrowanie działa po każdym znaku; dane pobierane są raz (jedno zapytanie w Network).

Podpowiedź

Pobrane dane w jednym sygnale, filtr w computed z filter i toLowerCase().includes(…).

ZAD 5Katalog z routerem★★★

Połącz lekcje 35 i 36: serwis pobiera listę wydarzeń z JSON-a, lista po lewej linkuje do wydarzenie/:id, a szczegóły szukają wydarzenia w danych serwisu. Wejście bezpośrednio na adres szczegółów (F5) też musi działać.

Kryterium sukcesu: lista i szczegóły korzystają z tych samych danych; po odświeżeniu strony na szczegółach dane się wczytują i karta się pokazuje.

Podpowiedź

Szczegóły też wywołują wczytaj(), a computed z find czyta serwis.lista() — przeliczy się, gdy dane dotrą.

ZAD 6Quiz z pliku★★★

Plik quiz.json z pięcioma pytaniami (pytanie, odpowiedzi — tablica, poprawna — numer). Aplikacja pobiera pytania, pokazuje je po jednym z odpowiedziami jako przyciskami, liczy punkty i na końcu pokazuje wynik oraz przycisk „Od nowa”.

Kryterium sukcesu: pytania pochodzą wyłącznie z pliku; dopisanie szóstego pytania do JSON-a wydłuża quiz bez zmiany kodu; jest obsługa ładowania i błędu.

Podpowiedź

Sygnały: pytania, nr, punkty; bieżące pytanie to computed(() => this.pytania()[this.nr()]).

PODSUMOWANIE

Co trzeba zapamiętać

  • Pliki z public są dostępne pod adresem od ukośnika: /dane/plik.json.
  • inject(HttpClient), potem http.get<Typ>(adres).subscribe(…) — bez subscribe zapytanie nie jest wysyłane.
  • Ładowanie, dane i błąd to trzy sygnały; subscribe({ next, error }) obsługuje oba zakończenia.
  • Zakładka Network pokazuje każde zapytanie, jego status i odpowiedź.
  • Pobieranie umieszczaj w serwisie, żeby kilka komponentów korzystało z jednego zapytania.

Sprawdź się: w zakładce Network zapytanie ma status 200, a aplikacja pokazuje komunikat o błędzie. Co sprawdzisz w pierwszej kolejności?

Dokumentacja: angular.dev — Making HTTP requests.

Postęp zapisuje się w tej przeglądarce.