Comunicació amb el servidor a Angular¶
Serveis a Angular¶
A Angular, els serveis són elements fonamentals que proporcionen dades i funcionalitats reutilitzables al llarg de l'aplicació. Normalment, els serveis gestionen operacions CRUD (Create, Read, Update, Delete) i permeten mantenir la lògica de negoci i la gestió de dades de manera centralitzada i persistent.
- Provisió d'informació: Els serveis proporcionen dades als components que les demanen.
- Operacions CRUD: Realitzen operacions bàsiques de creació, lectura, actualització i eliminació.
- Persistència de dades: Mantenen les dades de manera persistent entre components.
- Reutilitzables: Són reutilitzables en tota l'aplicació, promovent un codi net i modular.
Decorador \@Injectable¶
Les classes de servei a Angular estan decorades amb @Injectable(). Aquest decorador indica a l'injector de dependències d'Angular que ha de proporcionar una instància de la classe quan calgui. Exemples:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // Indica que no cal afegir-ho a providers
})
export class ProductService {
// Mètodes i lògica del servei
}
El decorador @Injectable assegura que Angular gestioni la instància del servei com un singleton, és a dir, una única instància compartida entre tots els components que la requereixin.
Si el servei es declara amb providedIn: 'root', no és necessari afegir-lo a providers perquè Angular s'encarregarà de la seva injecció automàtica.
Injecció de dependències¶
A Angular, la injecció de dependències (DI) permet als components sol·licitar serveis de manera eficient. En comptes de crear instàncies amb new, Angular construeix i proporciona els serveis via constructor:
import { Component } from '@angular/core';
import { ProductService } from './product.service';
@Component({
selector: 'app-product',
templateUrl: './product.component.html'
})
export class ProductComponent {
constructor(private productService: ProductService) { }
}
Aquest enfocament fa el codi més llegible i fàcil de mantenir, i garanteix l'ús d'una mateixa instància del servei.
HttpClientModule¶
Els serveis d'Angular sovint obtenen dades d'un servidor mitjançant HTTP. Per fer-ho, cal afegir HttpClientModule al mòdul principal (o a la configuració de bootstrap). No cal (i no s'hauria de) importar HttpClientModule dins d'un servei.
Per exemple, afegiu HttpClientModule a AppModule o al bootstrap:
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
// altres imports
],
bootstrap: [AppComponent]
})
export class AppModule {}
Serveis com a clients HTTP¶
Els serveis poden utilitzar HttpClient per fer peticions HTTP injectant-lo al constructor. Exemple correcte (sense importar HttpClientModule dins del servei):
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Product } from './product.model';
@Injectable({
providedIn: 'root'
})
export class ProductService {
private productURL = 'https://api.example.com/products';
constructor(private http: HttpClient) { }
getProducts(): Observable<Product[]> {
// Suposant que l'API retorna { products: Product[] }
return this.http.get<{ products: Product[] }>(this.productURL).pipe(
map(response => response.products)
);
}
}
En aquest exemple, getProducts fa una petició HTTP GET per obtenir una llista de productes i fa servir map de RxJS per transformar la resposta abans de retornar un Observable.
Enviament de dades amb POST¶
Per enviar dades al servidor, s'utilitza el mètode post de HttpClient. Exemple corregit:
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private loginURL = 'https://api.example.com/login';
private httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
})
};
constructor(private http: HttpClient) { }
login(credentials: { username: string, password: string }): Observable<{ token: string }> {
return this.http.post<{ token: string }>(this.loginURL, JSON.stringify(credentials), this.httpOptions);
}
}
En aquest exemple, login envia credencials al servidor amb una petició POST i configura les capçaleres per indicar JSON.
Dades asíncrones¶
El maneig de dades asíncrones és clau. Angular usa RxJS i els seus Observables, que ofereixen avantatges sobre les promeses:
- Valors múltiples: un Observable pot emetre diversos valors en el temps.
- Lazy: un Observable comença a executar-se quan algú s'hi subscriu.
- Cancel·lació: es pot cancel·lar una subscripció per aturar emissions.
- Operadors: RxJS disposa d'operadors (
map,filter,reduce, etc.) per transformar fluxos.
Promeses vs Observables¶
Tot i que es poden utilitzar promeses, Angular prefereix Observables per la seva flexibilitat (vegeu avantatges anteriors).
Ús d'operadors en Observables¶
Els operadors permeten transformar o filtrar dades. Exemple corregit que filtra productes amb preu > 20:
import { map } from 'rxjs/operators';
getProducts(): Observable<Product[]> {
return this.http.get<{ products: Product[] }>(this.productURL).pipe(
map(response => response.products.filter(product => product.price > 20))
);
}
Aquest enfoc assegura que filter s'apliqui a l'array de productes, no a l'Observable.
Processament de respostes d'Observables¶
Un Observable pot tenir múltiples subscriptors; només comença a emetre quan algú s'hi subscriu. subscribe() accepta tres funcions: èxit, error i finalització.
products: Product[] = [];
ngOnInit(): void {
this.productsService.getProducts().subscribe(
prods => this.products = prods, // èxit
error => console.error(error), // error
() => console.log('Products loaded') // finalització
);
}
Mostrar dades asíncrones¶
Per evitar errors quan les dades arriben tard:
- Crear objectes buits per evitar accés undefined.
- Mostrar contingut només quan estigui carregat (p. ex. *ngIf).
- Utilitzar l'operador ? (safe navigation) a les plantilles.
Signals¶
Les signals són una opció senzilla per reactivitat bàsica. A partir d'Angular 16+ s'han anat consolidant; a Angular 20 són totalment compatibles i sovint recomanades per casos simples:
import { signal, effect } from '@angular/core';
export class ExampleComponent {
num = signal(0);
constructor() {
effect(() => { console.log(`Valor de num: ${this.num()}`); });
}
updateNum() { this.num.update(n => n + 1); }
ngOnInit(): void { this.num.set(1); }
}
Resolver¶
Per obtenir dades abans d'accedir a una ruta, s'utilitza un Resolver que implementa resolve:
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ProductService } from './product.service';
import { Product } from './product.model';
@Injectable({
providedIn: 'root'
})
export class ProductResolver implements Resolve<Product | null> {
constructor(private productsService: ProductService, private router: Router) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Product | null> {
return this.productsService.getProduct(route.params.id).pipe(
catchError(error => {
this.router.navigate(['/products']);
return of(null);
})
);
}
}
Aquest resolver obté les dades abans de carregar la ruta i, en cas d'error, redirigeix a una ruta segura.
Configuració de rutes amb Resolver¶
const routes: Routes = [
{
path: 'product/edit/:id',
canActivate: [ProductDetailGuard],
canDeactivate: [LeavePageGuard],
resolve: { product: ProductResolver },
component: ProductEditComponent
},
// altres rutes
];
Autenticació amb Angular¶
La autenticació és crítica. Es pot gestionar amb cookies o tokens (p. ex. JWT).
Cookies i Tokens¶
- Cookies: enviades automàticament pel navegador si frontend i backend comparteixen domini.
- Tokens: s'envien manualment (normalment guardats a
localStorageosessionStorage) i són útils quan el frontend i backend estan en dominis diferents.
Interceptors¶
Els interceptors permeten modificar peticions HTTP abans d'enviar-les (p. ex. afegir token). Exemple corregit:
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthInterceptorService implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('idToken');
if (token) {
const authReq = req.clone({ url: req.url.concat(`?auth=${token}`) });
return next.handle(authReq);
}
return next.handle(req);
}
}
Per registrar l'interceptor al mòdul principal:
import { HTTP_INTERCEPTORS } from '@angular/common/http';
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptorService,
multi: true,
},
],
Guards¶
Els guards permeten permetre o denegar accés a rutes. Exemple de CanActivate:
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProductDetailGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const id = +route.params.id;
if (isNaN(id) || id < 1) {
console.log('La ID no és vàlida');
return this.router.parseUrl('/catalog');
}
return true;
}
}
Ruta amb guard:
{ path: 'product/:id', canActivate: [ProductDetailGuard], component: ProductDetailComponent },
Variables com a Observables¶
En aplicacions autenticades, els components han de reaccionar a canvis d'estat sense recarregar la pàgina. BehaviorSubject o Subject són útils per mantenir i observar l'estat d'autenticació.
Exemple amb BehaviorSubject:
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private loguedInfo: BehaviorSubject<boolean>;
constructor() {
this.loguedInfo = new BehaviorSubject<boolean>(false);
}
isLogued(): Observable<boolean> {
return this.loguedInfo.asObservable();
}
login() {
// Lògica d'autenticació
this.loguedInfo.next(true);
}
logout() {
// Lògica de tancament de sessió
this.loguedInfo.next(false);
}
}
Subscripció a l'estat en un component:
import { Component, OnInit } from '@angular/core';
import { AuthService } from './auth.service';
@Component({
selector: 'app-root',
template: `<div *ngIf="logued">Usuari autenticat</div>`
})
export class AppComponent implements OnInit {
logued = false;
constructor(private auth: AuthService) {}
ngOnInit(): void {
this.auth.isLogued().subscribe(logued => {
this.logued = logued;
});
}
}
Integració d'Angular amb Supabase¶
Supabase és un BaaS compatible amb TypeScript. Instal·lació:
npm install @supabase/supabase-js
Configuració d'entorn a src/environments/environment.ts:
export const environment = {
production: false,
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseKey: 'YOUR_SUPABASE_KEY',
};
Substituïu YOUR_SUPABASE_URL i YOUR_SUPABASE_KEY per les vostres credencials.
Exemple de servei supabase.service.ts:
import { Injectable } from '@angular/core';
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { environment } from '../environments/environment';
@Injectable({
providedIn: 'root',
})
export class SupabaseService {
private supabase: SupabaseClient;
constructor() {
this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey);
}
async getData(table: string) {
const { data, error } = await this.supabase.from(table).select('*');
if (error) {
console.error('Error fetching data:', error);
throw error;
}
return data;
}
async insertData(table: string, row: any) {
const { data, error } = await this.supabase.from(table).insert(row);
if (error) {
console.error('Error inserting data:', error);
throw error;
}
return data;
}
async updateData(table: string, row: any, id: number) {
const { data, error } = await this.supabase.from(table).update(row).eq('id', id);
if (error) {
console.error('Error updating data:', error);
throw error;
}
return data;
}
async deleteData(table: string, id: number) {
const { data, error } = await this.supabase.from(table).delete().eq('id', id);
if (error) {
console.error('Error deleting data:', error);
throw error;
}
return data;
}
}
Conversió de promeses a Observables¶
El SDK de Supabase és prometes; per integrar-lo amb RxJS es pot utilitzar from:
import { from, Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class SupabaseService {
private supabase: SupabaseClient;
constructor() {
this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey);
}
getDataObservable(table: string): Observable<any> {
return from(this.getData(table));
}
private async getData(table: string) {
const { data, error } = await this.supabase.from(table).select('*');
if (error) {
console.error('Error fetching data:', error);
throw error;
}
return data;
}
}
Exemple de component que consumeix l'observable:
import { Component, OnInit } from '@angular/core';
import { SupabaseService } from '../supabase.service';
@Component({
selector: 'app-data',
templateUrl: './data.component.html',
styleUrls: ['./data.component.css'],
})
export class DataComponent implements OnInit {
data: any[] = [];
constructor(private supabaseService: SupabaseService) {}
ngOnInit() {
this.supabaseService.getDataObservable('your_table_name').subscribe(
(data) => {
this.data = data;
},
(error) => {
console.error('Error loading data:', error);
}
);
}
}