Best practices per sviluppare in Angular
Per garantire che il tuo progetto Angular segua le best practices, dovresti concentrarti su diverse aree chiave che migliorano la sicurezza, le prestazioni e la manutenibilità.
In questo post raccontiamo quali sono quelle più importanti.
- Qualità del Codice
- Ottimizzazione delle Prestazioni
- Struttura e Organizzazione del Progetto
- Sicurezza
- Design dei Componenti
- Documentazione e Test
Qualità del Codice
- Naming convention: Mantenere una nomenclatura uniforme per file e variabili.
- Limitare la dimensione dei componenti: Mantenere i file sotto le 400 righe e le funzioni sotto le 75 righe per migliorare la leggibilità.
- Evitare l’uso estensivo tipo ‘any’: TypeScript è un linguaggio tipizzato, per cui è importante utilizzare tipi specifici per ridurre i bug e migliorare il refactoring.
Ottimizzazione delle Prestazioni
- Lazy Loading: Implementare il lazy loading per i moduli per migliorare i tempi di caricamento iniziali.
- Utilizzare
trackBy
conngFor
: Ottimizzare le prestazioni di rendering prevenendo aggiornamenti non necessari del DOM. Un esempio è il seguente:
Immagina di avere un file TypeScript di un componente (track-by-example.component.ts
) dove definisci un array di prodotti.
import { Component, OnInit } from '@angular/core';
export interface Product {
id: number;
name: string;
}
@Component({
selector: 'app-track-by-example',
templateUrl: './track-by-example.component.html',
})
export class TrackByExampleComponent implements OnInit {
products: Product[] = [
{ id: 1, name: 'Prodotto A' },
{ id: 2, name: 'Prodotto B' },
{ id: 3, name: 'Prodotto C' },
];
ngOnInit() {
// Emula l'aggiunta di un prodotto alla lista
setTimeout(() => {
this.products.push({ id: 4, name: 'Prodotto D' });
this.products[1].name = 'Prodotto B Aggiornato'; // Aggiorna un elemento esistente
}, 2000);
}
trackByProductId(index: number, product: Product): number {
return product.id; // ID unico per ogni prodotto
}
}
Esempio editato da Gregorio Giorgino
Nel file HTML del tuo componente (track-by-example.component.html
), utilizza la direttiva *ngFor insieme alla funzione trackBy.
<ul>
<li *ngFor="let product of products; trackBy: trackByProductId">
{{ product.name }}
</li>
</ul>
La funzione trackByProductId
restituisce un ID (ossia product.id
) per ogni elemento. Questo aiuta Angular a tenere traccia di quali elementi sono cambiati, stati aggiunti o rimossi, così che si possa ottimizzare il rendering aggiornando solo gli elementi del DOM che sono cambiati invece di caricare nuovamente l’intero elenco.
- Caching delle chiamate verso le API: Memorizzare nella cache le risposte delle API che non cambiano frequentemente per migliorare le prestazioni di risposte dell’applicazione. Ad esempio, è possibile usare RxJS per configure il caching delle chiamate, come visibile in questo esempio.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { shareReplay, catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ApiService {
private cache$: Observable<any>;
constructor(private http: HttpClient) {}
getData(apiUrl: string): Observable<any> {
if (!this.cache$) {
this.cache$ = this.http.get(apiUrl).pipe(
shareReplay(1),
catchError(error => {
console.error('Error fetching data:', error);
throw error;
})
);
}
return this.cache$;
}
clearCache() {
this.cache$ = null;
}
}
Il metodo getData
controlla se cache$
è già definito. In caso contrario, effettua una richiesta HTTP GET e memorizza nella cache il risultato tramite shareReplay(1)
, che memorizza l’ultimo valore emesso. L’operatore catchError
registra tutti gli errori che si verificano durante la richiesta, mentre il metodo clearCache
consente di svuotare la cache ogni volta che è necessario.
Struttura e Organizzazione del Progetto
- Organizzare per feature: Strutturare i file in base alle funzionalità piuttosto che ai componenti per migliorare la scalabilità.
- Utilizzare Angular CLI per generare componenti e servizi: Sfruttare Angular CLI per una configurazione coerente del progetto e generazione dei file, piuttosto che crearli manualmente.
Sicurezza
- Content Security Policy (CSP): Applicare una CSP rigorosa per mitigare i rischi di XSS. Questo significa, -ad esempio- utilizzare degli header specifici per far sì che il contenuto di una response venga caricato solo se l’origine della request coincide con il dominio dell’applicazione:
Content-Security-Policy: default-src 'self';
- Validazione e sanitization degli input: Validare gli input degli utenti per prevenire attacchi, utilizzando -ad esempio- i validatori messi a disposizione dai form Reactive.
- Usare sempre HTTPS: Assicurarsi che tutte le trasmissioni di dati siano crittografate utilizzando HTTPS.
- Usare degli
Interceptor
per sfruttare i token (come JWT) che controllino ogni request in ingresso, assicurando una comunicazione sicura con il back-end. - Usare dei
Route Guards
per gestire l’accesso ai componenti dell’applicazione in maniera granulare, a seconda dei ruoli dell’utente. - Aggiornamenti regolari delle dipendenze: Mantenere aggiornati Angular e le sue dipendenze per le patch di sicurezza.
Design dei Componenti
- Principio della Singola Responsabilità: Suddividere i componenti in parti più piccole e riutilizzabili. Una buona applicazione di questo pattern implica la creazione di componenti che si concentrano su un singolo compito o responsabilità. Ad esempio, considera un’applicazione Angular che gestisce un elenco di prodotti: questo dovrebbe mostrare l’elenco dei prodotti e quindi definire i soli elementi necessari a questa attività, come visibile di seguito:
@Component({
selector: 'app-item-list',
templateUrl: './item-list.component.html',
})
export class ItemListComponent {
@Input() items: Item[];
constructor(private itemService: ItemService) {}
ngOnInit(): void {
this.itemService.getItems().subscribe((items) => {
this.items = items;
});
}
}
In questo caso, dove il pattern non viene applicato, si va invece ad aggiungere della logica per poter aggiungere dei prodotti all’elenco, andando quindi ad aumentare le responsabilità del componente:
@Component({
selector: 'app-item-list',
templateUrl: './item-list.component.html',
})
export class ItemListComponent implements OnInit {
items: Item[] = [];
constructor(private itemService: ItemService) {}
ngOnInit(): void {
this.itemService.getItems().subscribe((items) => {
this.items = items;
});
}
addItem(name: string): void {
this.itemService.addItem(name).subscribe((item) => {
this.items.push(item);
});
}
}
Documentazione e Test
- Documentazione: Mantenere una documentazione chiara per il codice e i componenti, a partire dal README.md fino alla documentazione utente.
- Test Unitari: Implementare test unitari per garantire che la funzionalità rimanga intatta durante le modifiche.
E tu, conosci qualche altra best practice da condividere?