Kurs Angular · Moduł 9: HttpClient
Stacja gońców - HttpClient od pierwszego zwoju
W tej lekcji10
Dwa razy widziałeś już na szlaku linię this.http.get(...). Raz przy moście do sygnałów, gdy toSignal zamieniało odpowiedź serwera w dym na wieży, raz na szlakach ninja, gdy strumienie uczyły się czekać i filtrować. Za każdym razem ta linia po prostu działała, i za każdym razem omijaliśmy dwa pytania: skąd w klasie bierze się pole http oraz dlaczego ta sama linia, przepisana do świeżo założonej aplikacji, wywraca ją tuż po uruchomieniu.
Bo goniec to nie tylko człowiek z tobołkiem. Zanim ktokolwiek pobiegnie z zamku do zamku, w spisie warowni musi stać stacja pocztowa: miejsce, w którym gońcy czekają na rozkazy i z którego wolno ich zawołać każdemu domownikowi. Gdy takiej stacji nikt nie postawił, wołanie o gońca nie kończy się ciszą, tylko awarią - Angular przerywa budowę klasy i wypisuje błąd, w którym stoi linia NullInjectorError: No provider for HttpClient!. Poprosiłem o HttpClient pusty injektor, żeby zobaczyć ten komunikat na własne oczy, i brzmi on dokładnie tak.
Ta lekcja jest o postawieniu stacji i o wysłaniu pierwszego gońca po zwój. Niczego dziś nie zaniesiemy do zamku sąsiada - dziś tylko przynosimy.
Czego się nauczysz
- wpisywać stację gońców do spisu zamku funkcją
provideHttpClient() - dobierać jej cechy, na czele z
withFetch(), i wiedzieć, kiedy tej jednej cechy lepiej nie włączać - sprowadzać
HttpClientdo gildii jedną linią importu i jednym wywołanieminject() - wysyłać żądanie GET z zapowiedzią typu, jaki ma wrócić
- rozumieć, dlaczego z metody wychodzi strumień zamiast danych i dlaczego nikt nie biegnie, dopóki nikt nie słucha
- doklejać pytania do adresu klasą
HttpParams, a nie sklejaniem tekstu
Stacja pocztowa w spisie zamku
Miejsce, w którym stawia się stację, znasz już z modułu o szlakach. To plik app.config.ts, a w nim stała appConfig typu ApplicationConfig - obiekt konfiguracji wręczany aplikacji przy starcie. Ten typ ma dokładnie jedno pole: tablicę providers, czyli spis wszystkiego, co ma być do dyspozycji przez cały czas działania aplikacji. Mapę tras dopisywałeś do niego funkcją provideRouter. Stację gońców dopisuje się bliźniaczo: funkcją provideHttpClient, którą importujesz z pakietu @angular/common/http. Ona sama nie tworzy jeszcze żadnego gońca - zwraca gotowy komplet wpisów, który tablica providers przyjmuje tak samo jak wpisy routera.
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient()
]
};Od tej chwili każdy komponent i każdy serwis w aplikacji może poprosić o HttpClient i go dostanie. Zwróć jednak uwagę, jak niewiele się przy tym zmieniło: nie ruszyliśmy ani jednego komponentu, nie dopisaliśmy niczego do żadnej klasy, nie podaliśmy nawet adresu serwera. Stacja nie wie, dokąd będą biegać gońcy, i wiedzieć nie musi - adresy podasz dopiero przy konkretnych wywołaniach. Nie poleciało też jeszcze żadne żądanie i nie poleci, dopóki ktoś o nie nie poprosi.
Zapamiętaj nazwę funkcji dokładnie, bo trzy inne brzmią równie wiarygodnie i żadna nie zadziała. provideHttp() i importHttpClient() nie istnieją - sprawdziłem obie kompilatorem i każda kończy się błędem TS2305 z komunikatem, że moduł "@angular/common/http" nie eksportuje takiego składnika. Trzecia propozycja jest podstępniejsza, bo HttpClientModule istnieje naprawdę i spotkasz go w starszych przykładach z sieci. Należy jednak do świata modułów NgModule, a jego miejscem jest tablica imports dekoratora @NgModule - w app.config.ts nie ma czego takiego jak pole imports, bo ApplicationConfig ma wyłącznie providers. Próba dopisania tego pola kończy się błędem TS2353 mówiącym wprost, że imports nie istnieje w typie ApplicationConfig. Co więcej, w typach Angulara 19 sama klasa HttpClientModule jest oznaczona jako przestarzała, z adnotacją zalecającą w zamian provideHttpClient() w providers.
Cechy stacji, czyli po co nawias przy provideHttpClient
Puste nawiasy w provideHttpClient() nie są ozdobą. Ta funkcja przyjmuje dowolną liczbę cech - małych funkcji, które dokładają stacji jedną umiejętność. Ich nazwy zaczynają się od with, a poznasz ich w tym module kilka: withInterceptors stawia na trakcie rogatkę, której poświęcona jest osobna lekcja tego modułu, a withFetch podmienia sam sposób, w jaki goniec pokonuje drogę.
Domyślnie Angular wysyła żądania mechanizmem XMLHttpRequest - starym, sprawdzonym koniem, który wozi zwoje od czasów pierwszych przeglądarek. withFetch() każe zamiast niego używać nowszego Fetch API, wbudowanego dziś w każdą przeglądarkę i w Node.js od wersji 18. Cechę podaje się jako argument, więc wywołanie ma dwa piętra nawiasów: najpierw wołasz samą cechę, a jej wynik wkładasz do wywołania provideHttpClient.
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch())
]
};Najważniejsze jest to, co się nie zmieniło. Żaden serwis w aplikacji nie wygląda po tej zmianie inaczej, żadna metoda nie zmienia nazwy, żaden adres nie wymaga poprawki, a z wywołań nadal wychodzą te same strumienie. Podmieniliśmy wyłącznie konia pod gońcem - trasa, tobołek i rozkazy zostały nietknięte.
Dokumentacja typów Angulara 19 zaleca tę cechę wprost: przy aplikacjach renderowanych po stronie serwera radzi włączyć fetch dla lepszej wydajności i zgodności. Jest jednak jeden przypadek, w którym tej cechy się nie włącza, i wolę Ci go powiedzieć teraz niż po straconym popołudniu. Przy withFetch() stoi w tych samych typach notatka: Fetch API nie raportuje postępu wysyłki. Zajrzałem do skompilowanego pakietu i zgadza się co do joty - zdarzenie HttpEventType.UploadProgress emituje wyłącznie stara droga oparta na XMLHttpRequest, a droga oparta na fetch zna tylko postęp pobierania. Jeśli więc aplikacja ma pokazywać pasek postępu przy wysyłaniu pliku - a taki pasek zbudujesz już w następnej lekcji - to gałąź kodu z UploadProgress przy włączonym withFetch() nigdy się nie wykona, choć wszystko skompiluje się bez jednego ostrzeżenia. Moje zalecenie jest więc dwuczęściowe: w nowej aplikacji zaczynaj od provideHttpClient(withFetch()), zwłaszcza gdy renderujesz ją na serwerze, ale gdy w planach jest pasek postępu wysyłki, zostaw wywołanie bez tej cechy.
Jedna linia, którą warto umieć na pamięć
Stacja stoi, więc czas sprowadzić gońca do gildii. Klasa HttpClient mieszka w pakiecie @angular/common/http - nie w @angular/core, gdzie mieszkają inject, signal czy Injectable. To nie jest drobiazg do zapamiętania na siłę: sprawdziłem import z @angular/core kompilatorem i kończy się on błędem TS2305 o braku takiego eksportu. Linia importu wygląda tak i tylko tak.
import { HttpClient } from '@angular/common/http';Przeczytaj ją token po tokenie, bo kolejność jest niezmienna. Najpierw słowo kluczowe import. Potem nazwa w klamrach - klamry są konieczne, bo HttpClient jest eksportem nazwanym, jednym z wielu w tym pakiecie, a nie eksportem domyślnym. Potem słowo from, które oddziela to, co bierzesz, od tego, skąd to bierzesz. Na końcu ścieżka pakietu w apostrofach i średnik. Niczego tu nie da się przestawić: from przed klamrami nie ma czego oddzielać, a ścieżka bez from jest tylko luźnym napisem.
Zwróć uwagę, czego ta linia nie robi. Nie tworzy gońca. Sprowadza do pliku samą klasę - wzorzec i zarazem żeton, którym za chwilę poprosimy o gotową instancję.
Czym jest samuraj w kodzie
Zanim wyślemy pierwszego gońca, umówmy się, po co go wysyłamy. Rejestr klanu trzyma wpisy o wojownikach, a każdy wpis to obiekt z trzema polami: numerem id nadanym przez pisarza w zamku, imieniem name i nazwą klanu clan. Numer jest liczbą, dwa pozostałe pola są napisami. Opisuje to interfejs TypeScriptu, czyli znany Ci spod bramy Akademii sposób na powiedzenie kompilatorowi, jak wygląda obiekt.
interface Samurai {
id: number;
name: string;
clan: string;
}Interfejs nie tworzy żadnego obiektu i nie trafia do przeglądarki - znika przy kompilacji, a zostaje po nim wyłącznie wiedza kompilatora. Ma nam służyć do jednej rzeczy: żeby po powrocie gońca edytor podpowiadał clan zamiast milczeć, i żeby literówka w nazwie pola została złapana zanim ktokolwiek uruchomi aplikację.
Goniec w gildii, czyli inject zamiast new
Serwis, do którego wstawimy gońca, wygląda dokładnie jak gildie rzemieślnicze z modułu piątego. Dekorator @Injectable z opcją providedIn: 'root' oznacza jedną gildię na całe królestwo. Pole http wypełnia funkcja inject() z pakietu @angular/core, której podajesz klasę jako żeton, a ona oddaje gotową instancję ze stacji postawionej w konfiguracji. Pole apiUrl trzyma wspólny początek adresu, żeby nie powtarzać go w każdej metodzie.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class SamuraiService {
private http = inject(HttpClient);
private apiUrl = '/api/samurais';
}Klasa jest na razie pusta i tak ma być - powstało samo miejsce, do którego zaraz dołożymy metody. Popatrz, co się tu nie zmieniło względem gildii, które budowałeś w dzielnicy rzemieślników: ten sam dekorator, to samo providedIn: 'root', to samo inject(). To, że ten serwis rozmawia z serwerem, nie wymusiło ani jednej dodatkowej ceremonii. Zmieniła się wyłącznie klasa podana do inject().
Trzy inne sposoby na zdobycie gońca kuszą i wszystkie trzy sprawdziłem kompilatorem. new HttpClient() kończy się błędem TS2554 z komunikatem, że oczekiwano jednego argumentu, a podano zero - bo HttpClient potrzebuje do pracy mechanizmu wysyłkowego, który normalnie wręcza mu Angular. Nawet gdybyś ten argument dopisał, powstałby goniec spoza stacji: pomijający rogatki, o których będzie osobna lekcja, i całą konfigurację z app.config.ts. HttpClient.getInstance() kończy się błędem TS2339 o treści, że właściwość getInstance nie istnieje w typie typeof HttpClient - to wzorzec z innych języków, którego Angular nie używa, bo od wydawania instancji ma wstrzykiwanie zależności. Zostaje pomysł trzeci: skoro klasa jest już zaimportowana, może wystarczy wołać metody wprost na niej? Nie wystarczy - HttpClient.get('/api/samurais') to ten sam błąd TS2339, tym razem o właściwości get. Metoda get należy do instancji, a import przynosi tylko klasę. Instancję daje wyłącznie wywołanie inject(HttpClient) - a to, czy jego wynik zapiszesz do pola klasy, jak wyżej, czy do zwykłej stałej przez const, jest już kwestią wygody i niczego w mechanizmie nie zmienia.
Pierwszy zwój, czyli get
Pierwsza metoda ma przynieść cały rejestr. Nazwiemy ją getAll, a w środku wywołamy na gońcu metodę get(), podając jej adres. W nawiasach ostrych przed adresem dopisujemy typ, jakiego spodziewamy się w odpowiedzi - tutaj tablicy samurajów, czyli Samurai[]. Typ zwracany przez samą metodę to Observable<Samurai[]>, a klasę Observable importujemy z rxjs, dokładnie jak na szlakach ninja.
getAll(): Observable<Samurai[]> {
return this.http.get<Samurai[]>(this.apiUrl);
}Zapis get<Samurai[]> to obietnica złożona kompilatorowi, a nie kontrola na bramie. Angular nie sprawdza, czy serwer naprawdę przysłał tablicę wpisów - podstawiłem pod gońca fałszywego pisarza, który zamiast tablicy odesłał obiekt { oops: ... }, i strumień oddał ten obiekt bez mrugnięcia okiem, mimo że w kodzie stało Samurai[]. Nawiasy ostre pomagają Tobie i edytorowi, ale nie chronią przed serwerem, który zmienił zdanie. Gdyby kształt odpowiedzi był naprawdę niepewny, sprawdzenie trzeba dopisać samodzielnie.
Zwróć też uwagę na to, czego ta metoda nie zrobiła. Po jej wywołaniu nie poleciało żadne żądanie. Powstał tylko opis podróży - kto ma biec, dokąd i po co.
Dlaczego wraca strumień, a nie dane
Tu kryje się pytanie, na którym potyka się najwięcej osób: co właściwie zwraca http.get()? Odpowiedź brzmi: Observable, czyli strumień znany Ci z modułu o zwiadowcach. Nie Promise - sprawdziłem to na działającym kliencie i zwrócony obiekt nie ma metody then. Nie dane - bo droga do zamku sąsiada trwa, a metoda kończy się natychmiast, więc gdyby miała zwracać wpisy, musiałaby je wcześniej mieć. I nie Callback - metoda get() nie przyjmuje żadnej funkcji do wywołania później, w nawiasie stoi wyłącznie adres i opcjonalne opcje.
Różnica między strumieniem a obietnicą bywa zdradliwa, więc sprawdziłem i to: await postawione przed takim wywołaniem niczego nie załatwi - zamiast zaczekać na odpowiedź, odda po prostu ten sam strumień. Żeby doczekać się danych, trzeba albo zasubskrybować, albo świadomie zamienić strumień na obietnicę osobną funkcją z rxjs.
Funkcję podajesz dopiero przy subskrypcji, i dopiero ona wysyła gońca w drogę. W komponencie wygląda to tak: bierzesz serwis przez inject(), a w metodzie cyklu życia ngOnInit, którą znasz z modułu o komponentach, subskrybujesz strumień.
import { Component, inject, OnInit } from '@angular/core';
import { SamuraiService } from './samurai.service';
@Component({
selector: 'app-roster',
standalone: true,
template: '<p>Rejestr klanu</p>'
})
export class RosterComponent implements OnInit {
private samurai = inject(SamuraiService);
ngOnInit(): void {
this.samurai.getAll().subscribe(list => {
console.log('Wpisow w rejestrze:', list.length);
});
}
}Dopiero ta jedna linia z subscribe wypycha gońca za bramę. Sprawdziłem to na podstawionym pisarzu, który liczył wizyty: przed subskrypcją licznik pokazywał zero, po pierwszej subskrypcji jeden, a po dodaniu drugiej - dwa. Strumienie HTTP są leniwe i zimne: dwie subskrypcje tego samego strumienia to dwa osobne żądania, a nie jedna odpowiedź podana dwóm słuchaczom. Gdybyś więc kusił się o zapisanie strumienia do pola i subskrybowanie go w dwóch miejscach szablonu, wyślesz dwóch gońców po ten sam zwój.
Sam subscribe jest tu po to, żeby mechanika była widoczna. W prawdziwym komponencie sięgniesz raczej po AsyncPipe ze szlaków ninja albo po toSignal, którym budowałeś most z wieżą sygnałową - obie drogi subskrybują za Ciebie i obie sprzątają subskrypcję, gdy komponent znika z ekranu.
Pytanie doklejone do adresu
Rejestr klanu bywa długi, więc rzadko chcemy całości. Chcemy wpisów pasujących do frazy, czasem zawężonych do jednego klanu. Takie warunki dopisuje się do adresu po znaku zapytania i kusi, żeby skleić je zwykłym tekstem. Nie rób tego: fraza "ronin z gór" wsadzona wprost do adresu zostawia w nim spacje i polskie znaki, a znak & we frazie rozbije adres na dwa parametry. Od bezpiecznego składania służy klasa HttpParams z tego samego pakietu co HttpClient. Tworzysz pusty komplet przez new HttpParams(), a kolejne pary dokładasz metodą set(), podając nazwę parametru i wartość. Gotowy komplet wkładasz do obiektu opcji jako pole params.
search(query: string, clan?: string): Observable<Samurai[]> {
let params = new HttpParams().set('q', query);
if (clan) {
params = params.set('clan', clan);
}
return this.http.get<Samurai[]>(this.apiUrl, { params });
}Zwróć uwagę na let i na przypisanie params = params.set(...), bo to nie jest ozdobnik. HttpParams jest niezmienny: set() nie dopisuje niczego do obiektu, na którym je wołasz, tylko zwraca nowy komplet. Sprawdziłem to - po wywołaniu set('clan', ...) stary obiekt nadal miał jeden parametr, a drugi znalazł się wyłącznie w zwróconej kopii. Gdybyś zapisał samo params.set('clan', clan); bez przypisania, zawężenie do klanu po prostu by zniknęło, a kompilator nie powiedziałby ani słowa.
Co ta klasa robi z wartościami, sprawdziłem na gotowym żądaniu: fraza z polskimi znakami i spacjami pojechała jako q=ronin%20z%20g%C3%B3r&clan=Takeda. Zamiana na taki zapis to jedyna rzecz, jaka się tu wydarzyła - nadal jest to zwykły GET pod ten sam adres, bez ładunku i bez dodatkowych nagłówków. Parametry są częścią adresu, a nie treści.
Jeden wpis, jeden adres
Ostatni typowy przypadek to jeden wojownik po numerze. Metoda niczym nowym się nie różni - to nadal get() i nadal ten sam typ w nawiasach ostrych, tyle że w liczbie pojedynczej. Zmienia się wyłącznie adres, bo numer trzeba dokleić do ścieżki. Składamy go szablonem tekstowym: zapisem w odwrotnych apostrofach, w którym miejsca na wartości oznacza się przez ${...}.
getById(id: number): Observable<Samurai> {
return this.http.get<Samurai>(`${this.apiUrl}/${id}`);
}Pilnuj odwrotnych apostrofów, bo to najczęstsza pomyłka w całej tej sekcji i taka, której kompilator nie zgłosi. Sprawdziłem, co się dzieje po zwykłych apostrofach: żądanie poleciało pod adres /api/samurais/${id} z literalnym zapisem zamiast liczby, więc serwer dostał ścieżkę, której nie zna. Przy poprawnym szablonie adres brzmiał /api/samurais/7. Nie zmieniło się przy tym nic poza adresem - to nadal ten sam goniec, ta sama metoda i ten sam leniwy strumień, który ruszy dopiero przy subskrypcji.
Pamiętaj: stacja w app.config.ts daje aplikacji gońców, inject(HttpClient) przydziela jednego Twojej gildii, a get() tylko spisuje mu rozkaz - w drogę wyprawia go dopiero ten, kto zacznie słuchać.
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 metoda http.get() w Angular?
2. Jak we współczesnym Angularze 19 najczęściej uzyskujemy instancję HttpClient w serwisie?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w poziomie
Ułóż tokeny jednej linii importujące HttpClient z biblioteki Angulara:
- Edytor kodu
Uzupełnij serwis pobierający listę samurajów z API.
- Klikanie w kolejności
Zbuduj wywołanie konfigurujące HttpClient z obsługą Fetch API (withFetch):