Angular + PWA: Creare un'App Installabile
Le Progressive Web App (PWA) rappresentano il ponte tra le applicazioni web e le app native. Grazie a tecnologie come i Service Worker, il Web App Manifest e le Push Notification, una PWA offre esperienze paragonabili a quelle di un'app installata dallo store, ma senza i costi e le complessità della distribuzione nativa. Angular integra il supporto PWA in modo nativo, rendendo la trasformazione di un'applicazione web in un'app installabile un processo semplice e ben documentato.
In questo ottavo articolo della serie Angular Moderno esploreremo ogni aspetto della creazione di una PWA con Angular: dalla configurazione iniziale alla gestione avanzata della cache, dal supporto offline alle notifiche push, fino ai meccanismi di aggiornamento automatico e all'install prompt personalizzato. Vedremo come testare e debuggare ogni funzionalità con gli strumenti di Chrome DevTools.
Cosa Imparerai in Questo Articolo
- Cosa sono le PWA e quali vantaggi offrono rispetto alle app native
- Come configurare Angular PWA con
ng add @angular/pwa - Personalizzazione del Web App Manifest per icone, colori e modalità di visualizzazione
- Configurazione del Service Worker con
ngsw-config.json - Strategie di caching: prefetch, lazy, network-first, cache-first
- Supporto offline con fallback pages e IndexedDB
- Push notification con il servizio
SwPush - Gestione degli aggiornamenti con
SwUpdate - Install prompt personalizzato con
beforeinstallprompt - Testing e debugging con Chrome DevTools e Lighthouse
Panoramica della Serie Angular Moderno
| # | Articolo | Focus |
|---|---|---|
| 1 | Angular Signals | Reattività fine-grained |
| 2 | Zoneless Change Detection | Eliminare Zone.js |
| 3 | Nuovi Template @if, @for, @defer | Control flow moderno |
| 4 | Standalone Components | Architettura senza NgModule |
| 5 | Signal Forms | Form reattivi con Signals |
| 6 | SSR e Incremental Hydration | Server-Side Rendering |
| 7 | Core Web Vitals in Angular | Performance e metriche |
| 8 | Sei qui - Angular PWA | Progressive Web App |
| 9 | Dependency Injection Avanzata | DI tree-shakable |
| 10 | Migrare da Angular 17 a 21 | Guida alla migrazione |
1. Cosa Sono le Progressive Web App
Una Progressive Web App è un'applicazione web che utilizza tecnologie moderne per offrire un'esperienza utente paragonabile a quella di un'app nativa. Il termine "progressive" indica che l'app funziona per ogni utente, indipendentemente dal browser utilizzato, migliorando progressivamente l'esperienza sui browser che supportano le funzionalità avanzate.
Caratteristiche Chiave delle PWA
Le PWA si distinguono dalle tradizionali applicazioni web per una serie di capacità che le avvicinano alle app native distribuite tramite gli store.
PWA vs App Nativa vs Web App Tradizionale
| Caratteristica | Web App | PWA | App Nativa |
|---|---|---|---|
| Installabile | No | Si | Si |
| Funziona offline | No | Si | Si |
| Push notification | No | Si | Si |
| Aggiornamento automatico | Si | Si | Tramite store |
| Accesso hardware | Limitato | Buono | Completo |
| Distribuzione | URL | URL + Install | App Store |
| Costo di sviluppo | Basso | Basso | Alto |
| SEO | Si | Si | No (store indexing) |
Quando Scegliere una PWA
Le PWA sono la scelta ideale quando si vuole raggiungere il maggior numero di utenti con un singolo codebase. Sono perfette per e-commerce, blog, dashboard aziendali, tool di produttività e qualsiasi applicazione che beneficia dell'accesso offline e delle notifiche push. Se l'app richiede accesso profondo all'hardware (Bluetooth avanzato, NFC, sensori specifici), un'app nativa potrebbe essere ancora necessaria.
2. Setup Angular PWA
Angular fornisce uno schematic ufficiale che automatizza la configurazione PWA. Con un singolo comando, la CLI genera tutti i file necessari: il manifest, la configurazione del service worker e le icone di default.
# Aggiungere PWA al progetto
ng add @angular/pwa
# Cosa viene generato:
# - src/manifest.webmanifest (Web App Manifest)
# - ngsw-config.json (configurazione Service Worker)
# - src/assets/icons/ (icone PWA in varie dimensioni)
# - Modifiche a angular.json (serviceWorker: true)
# - Modifiche a index.html (link al manifest, meta theme-color)
# - Modifiche a app.config.ts (ServiceWorkerModule / provideServiceWorker)
Configurazione in app.config.ts
Dopo aver eseguito lo schematic, il file app.config.ts viene aggiornato
automaticamente per registrare il service worker. La registrazione avviene solo in
produzione e dopo la stabilizzazione dell'applicazione.
// app.config.ts
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideServiceWorker } from '@angular/service-worker';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(),
// Registra il SW dopo la stabilizzazione dell'app
registrationStrategy: 'registerWhenStable:30000'
})
]
};
Service Worker e Ambiente di Sviluppo
Il service worker è disabilitato in modalità sviluppo (isDevMode()) perchè
il caching aggressivo interferirebbe con il hot-reload e il live-reloading. Per testare
il service worker localmente, devi eseguire una build di produzione con
ng build e servire i file statici con un server HTTP come
http-server o npx serve.
3. Web App Manifest
Il Web App Manifest è un file JSON che descrive la tua applicazione al browser e al sistema operativo. Contiene informazioni come il nome dell'app, le icone, i colori del tema e la modalità di visualizzazione. Senza un manifest valido, il browser non offre la possibilità di installare l'app.
{
"name": "La Mia App Angular PWA",
"short_name": "MiaApp",
"description": "Un'applicazione Angular PWA completa con supporto offline",
"theme_color": "#58a6ff",
"background_color": "#0d1117",
"display": "standalone",
"orientation": "portrait-primary",
"scope": "/",
"start_url": "/",
"id": "/",
"categories": ["productivity", "utilities"],
"icons": [
{
"src": "assets/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"screenshots": [
{
"src": "assets/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide",
"label": "Dashboard principale"
},
{
"src": "assets/screenshots/mobile.png",
"sizes": "750x1334",
"type": "image/png",
"form_factor": "narrow",
"label": "Vista mobile"
}
],
"shortcuts": [
{
"name": "Dashboard",
"short_name": "Dash",
"url": "/dashboard",
"icons": [{ "src": "assets/icons/shortcut-dashboard.png", "sizes": "96x96" }]
},
{
"name": "Impostazioni",
"short_name": "Settings",
"url": "/settings",
"icons": [{ "src": "assets/icons/shortcut-settings.png", "sizes": "96x96" }]
}
]
}
Modalità Display del Manifest
| Modalità | Descrizione | Caso d'Uso |
|---|---|---|
standalone |
L'app appare come un'app nativa senza barra degli indirizzi | La scelta più comune per PWA |
fullscreen |
L'app occupa tutto lo schermo, senza UI del browser | Giochi, presentazioni immersive |
minimal-ui |
Simile a standalone ma con controlli di navigazione minimi | App che necessitano di navigazione back/forward |
browser |
L'app si apre in una tab del browser standard | Siti web tradizionali (non raccomandato per PWA) |
4. Configurazione del Service Worker
Il file ngsw-config.json è il cuore della configurazione PWA in Angular.
Definisce quali risorse vengono messe in cache, come vengono aggiornate e con quale
strategia. Angular genera automaticamente il file ngsw-worker.js durante
la build di produzione basandosi su questa configurazione.
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app-shell",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
]
}
}
],
"dataGroups": [
{
"name": "api-freshness",
"urls": ["/api/user/**", "/api/notifications/**"],
"cacheConfig": {
"strategy": "freshness",
"maxSize": 50,
"maxAge": "1h",
"timeout": "5s"
}
},
{
"name": "api-performance",
"urls": ["/api/articles/**", "/api/products/**"],
"cacheConfig": {
"strategy": "performance",
"maxSize": 100,
"maxAge": "1d"
}
}
],
"navigationUrls": [
"/**",
"!/__**"
]
}
assetGroups vs dataGroups
| Proprietà | assetGroups | dataGroups |
|---|---|---|
| Scopo | Risorse statiche dell'app (JS, CSS, immagini) | Dati dinamici da API |
| Versionamento | Basato sull'hash del file | Basato su tempo (maxAge) |
| installMode | prefetch o lazy |
N/A |
| strategy | N/A | freshness o performance |
| Aggiornamento | Quando cambia la versione dell'app | In base a maxAge e timeout |
5. Strategie di Caching
La scelta della strategia di caching è fondamentale per bilanciare performance e
freschezza dei dati. Angular NGSW supporta due strategie principali per i
dataGroups: freshness (network-first) e
performance (cache-first), oltre al caching statico per gli asset.
Strategie di Caching nel Dettaglio
| Strategia | Priorità | Comportamento | Caso d'Uso |
|---|---|---|---|
| prefetch (assetGroups) | Cache | Scarica tutte le risorse subito all'installazione del SW | App shell, JS e CSS principali |
| lazy (assetGroups) | Cache on demand | Mette in cache la risorsa solo quando viene richiesta | Immagini, font, asset secondari |
| freshness (dataGroups) | Network | Prova la rete; se timeout scaduto, usa la cache | Profilo utente, notifiche, dati real-time |
| performance (dataGroups) | Cache | Usa la cache se disponibile e non scaduta; altrimenti rete | Articoli blog, catalogo prodotti, dati statici |
La strategia freshness è equivalente al pattern network-first: il service worker tenta prima la rete e, se la richiesta non riesce entro il timeout specificato, serve la versione in cache. La strategia performance è invece cache-first: serve immediatamente la versione in cache (se non scaduta) e aggiorna la cache in background.
Stale-While-Revalidate con Angular
Angular NGSW non supporta nativamente la strategia stale-while-revalidate,
ma puoi simularla combinando la strategia performance con un
maxAge breve. Ad esempio, impostando "maxAge": "5m" la cache
servira i dati immediatamente, ma dopo 5 minuti effettuera una nuova richiesta alla rete
al successivo accesso. Se necessiti di un controllo più granulare, considera l'uso
diretto della Cache API con un service worker personalizzato.
6. Supporto Offline
Una delle caratteristiche più importanti di una PWA è la capacità di funzionare senza connessione a internet. Angular gestisce automaticamente il caching delle risorse statiche tramite il service worker, ma per un'esperienza offline completa è necessario gestire anche lo stato di connessione, le pagine di fallback e la persistenza dei dati locali.
Rilevamento dello Stato di Connessione
// network-status.service.ts
import { Injectable, signal, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class NetworkStatusService {
private platformId = inject(PLATFORM_ID);
isOnline = signal(true);
connectionType = signal<string>('unknown');
constructor() {
if (isPlatformBrowser(this.platformId)) {
this.isOnline.set(navigator.onLine);
window.addEventListener('online', () => {
this.isOnline.set(true);
this.syncPendingData();
});
window.addEventListener('offline', () => {
this.isOnline.set(false);
});
// Network Information API (se disponibile)
const connection = (navigator as any).connection;
if (connection) {
this.connectionType.set(connection.effectiveType);
connection.addEventListener('change', () => {
this.connectionType.set(connection.effectiveType);
});
}
}
}
private async syncPendingData(): Promise<void> {
// Sincronizza i dati salvati localmente quando torna online
const pendingActions = await this.getPendingActions();
for (const action of pendingActions) {
try {
await fetch(action.url, action.options);
await this.removePendingAction(action.id);
} catch (err) {
console.error('Sync fallita per:', action.id);
}
}
}
private async getPendingActions(): Promise<any[]> {
// Recupera da IndexedDB le azioni in attesa
return [];
}
private async removePendingAction(id: string): Promise<void> {
// Rimuovi azione completata da IndexedDB
}
}
Persistenza Dati con IndexedDB
Per gestire dati strutturati offline, IndexedDB è la soluzione più robusta.
A differenza di localStorage (sincrono, limite 5-10 MB), IndexedDB supporta
grandi volumi di dati, query indicizzate e operazioni asincrone.
// offline-storage.service.ts
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class OfflineStorageService {
private db: IDBDatabase | null = null;
private platformId = inject(PLATFORM_ID);
private readonly DB_NAME = 'pwa-offline-store';
private readonly DB_VERSION = 1;
async init(): Promise<void> {
if (!isPlatformBrowser(this.platformId)) return;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
// Crea object stores per i dati offline
if (!db.objectStoreNames.contains('articles')) {
db.createObjectStore('articles', { keyPath: 'id' });
}
if (!db.objectStoreNames.contains('pendingActions')) {
const store = db.createObjectStore('pendingActions', {
keyPath: 'id', autoIncrement: true
});
store.createIndex('timestamp', 'timestamp');
}
};
request.onsuccess = (event) => {
this.db = (event.target as IDBOpenDBRequest).result;
resolve();
};
request.onerror = () => reject(request.error);
});
}
async save<T>(storeName: string, data: T): Promise<void> {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(storeName, 'readwrite');
tx.objectStore(storeName).put(data);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
async getAll<T>(storeName: string): Promise<T[]> {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(storeName, 'readonly');
const request = tx.objectStore(storeName).getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
Limiti dello Storage Offline
- Quota browser: I browser limitano lo storage disponibile (tipicamente 50-80% dello spazio disco). Usa
navigator.storage.estimate()per verificare lo spazio disponibile - Eviction policy: In caso di spazio insufficiente, il browser può eliminare i dati. Richiedi lo storage persistente con
navigator.storage.persist() - SSR safety: IndexedDB non esiste lato server. Usa sempre
isPlatformBrowserprima di accedervi - Conflitti di sincronizzazione: Quando l'utente torna online, i dati locali potrebbero confliggere con quelli del server. Implementa una strategia di risoluzione dei conflitti
7. Push Notification
Le notifiche push permettono alla tua PWA di inviare messaggi all'utente anche quando
l'app non è attiva. Angular fornisce il servizio SwPush che semplifica
la gestione delle sottoscrizioni e la ricezione delle notifiche tramite il
Web Push Protocol.
Sottoscrizione alle Notifiche Push
// push-notification.service.ts
import { Injectable, inject } from '@angular/core';
import { SwPush } from '@angular/service-worker';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class PushNotificationService {
private swPush = inject(SwPush);
private http = inject(HttpClient);
// Chiave pubblica VAPID (generata sul server)
private readonly VAPID_PUBLIC_KEY = 'BPxGEaVr...tua-chiave-pubblica...';
async subscribeToNotifications(): Promise<void> {
try {
// Richiedi il permesso e ottieni la sottoscrizione
const subscription = await this.swPush.requestSubscription({
serverPublicKey: this.VAPID_PUBLIC_KEY
});
console.log('Sottoscrizione push:', JSON.stringify(subscription));
// Invia la sottoscrizione al backend
await this.http.post('/api/push/subscribe', subscription).toPromise();
} catch (err) {
console.error('Errore sottoscrizione push:', err);
}
}
listenForNotifications(): void {
// Notifiche ricevute mentre l'app e in primo piano
this.swPush.messages.subscribe((message: any) => {
console.log('Notifica ricevuta:', message);
// Aggiorna la UI con i nuovi dati
});
// Clic sulla notifica (apre/focalizza l'app)
this.swPush.notificationClicks.subscribe((event) => {
console.log('Clic su notifica:', event.action, event.notification);
// Naviga alla pagina appropriata
const url = event.notification.data?.url;
if (url) {
window.open(url, '_blank');
}
});
}
async unsubscribe(): Promise<void> {
try {
const subscription = await this.swPush.subscription.toPromise();
if (subscription) {
await subscription.unsubscribe();
await this.http.post('/api/push/unsubscribe', {
endpoint: subscription.endpoint
}).toPromise();
}
} catch (err) {
console.error('Errore cancellazione sottoscrizione:', err);
}
}
}
Invio Notifiche dal Server
// server/push-server.ts (Node.js con web-push)
import webPush from 'web-push';
// Configurazione VAPID
webPush.setVapidDetails(
'mailto:info@example.com',
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
);
async function sendNotification(
subscription: webPush.PushSubscription,
payload: object
): Promise<void> {
const notification = {
notification: {
title: 'Nuovo articolo disponibile!',
body: 'Scopri le novità su Angular PWA',
icon: '/assets/icons/icon-192x192.png',
badge: '/assets/icons/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
url: '/blog/angular-pwa',
dateOfArrival: Date.now()
},
actions: [
{ action: 'leggi', title: 'Leggi Ora' },
{ action: 'chiudi', title: 'Chiudi' }
]
}
};
try {
await webPush.sendNotification(
subscription,
JSON.stringify(notification)
);
} catch (error: any) {
if (error.statusCode === 410) {
// Sottoscrizione scaduta: rimuovila dal database
await removeSubscription(subscription.endpoint);
}
}
}
VAPID Keys
Le chiavi VAPID (Voluntary Application Server Identification) servono per
autenticare il tuo server presso il servizio push del browser. Puoi generarle con il
comando npx web-push generate-vapid-keys. La chiave pubblica va inserita
nel frontend, quella privata resta esclusivamente sul server. Non committare mai la
chiave privata nel repository.
8. Gestione degli Aggiornamenti
Quando rilasci una nuova versione della tua PWA, il service worker deve scaricare le
risorse aggiornate e notificare l'utente. Angular fornisce il servizio
SwUpdate per gestire il ciclo di vita degli aggiornamenti in modo
controllato.
// app-update.service.ts
import {
Injectable, inject, ApplicationRef
} from '@angular/core';
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { filter, first, interval, concat } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AppUpdateService {
private swUpdate = inject(SwUpdate);
private appRef = inject(ApplicationRef);
initUpdateCheck(): void {
if (!this.swUpdate.isEnabled) {
console.log('Service Worker non abilitato');
return;
}
// Controlla aggiornamenti ogni 6 ore
const appIsStable$ = this.appRef.isStable.pipe(
first(stable => stable)
);
const every6Hours$ = interval(6 * 60 * 60 * 1000);
const every6HoursOnceAppStable$ = concat(appIsStable$, every6Hours$);
every6HoursOnceAppStable$.subscribe(async () => {
try {
const updateFound = await this.swUpdate.checkForUpdate();
console.log(updateFound
? 'Nuovo aggiornamento trovato!'
: 'App già aggiornata');
} catch (err) {
console.error('Errore controllo aggiornamento:', err);
}
});
// Ascolta quando una nuova versione e pronta
this.swUpdate.versionUpdates.pipe(
filter((event): event is VersionReadyEvent =>
event.type === 'VERSION_READY'
)
).subscribe(event => {
console.log('Versione corrente:', event.currentVersion.hash);
console.log('Nuova versione:', event.latestVersion.hash);
this.promptUpdate();
});
// Gestisci errori irrecuperabili del SW
this.swUpdate.unrecoverable.subscribe(event => {
console.error('SW in stato irrecuperabile:', event.reason);
// Forza il reload completo
document.location.reload();
});
}
private promptUpdate(): void {
const shouldUpdate = confirm(
'Una nuova versione dell\'app è disponibile. ' +
'Vuoi aggiornare ora?'
);
if (shouldUpdate) {
// Attiva la nuova versione e ricarica
this.swUpdate.activateUpdate().then(() => {
document.location.reload();
});
}
}
}
Ciclo di Vita di un Aggiornamento PWA
| Fase | Evento SwUpdate | Descrizione |
|---|---|---|
| 1. Rilevamento | VERSION_DETECTED |
Il SW ha trovato una nuova versione sul server |
| 2. Download | VERSION_INSTALLATION_FAILED |
Errore durante il download della nuova versione (se fallito) |
| 3. Pronta | VERSION_READY |
La nuova versione è scaricata e pronta per l'attivazione |
| 4. Attivazione | activateUpdate() |
L'utente conferma e il SW attiva la nuova versione |
| 5. Ricarica | location.reload() |
La pagina si ricarica con le risorse aggiornate |
Errore Irrecuperabile del Service Worker
L'evento unrecoverable indica che il service worker non riesce più a
servire correttamente la versione corrente dell'app (ad esempio, il server ha rimosso
risorse ancora necessarie). In questo caso, l'unica soluzione è forzare un ricaricamento
completo della pagina con document.location.reload(). Monitora questo evento
in produzione per identificare problemi di deploy.
9. Install Prompt Personalizzato
I browser mostrano automaticamente un banner di installazione quando la PWA soddisfa
i criteri di installabilita. Tuttavia, il banner nativo è poco visibile e controllabile.
Intercettando l'evento beforeinstallprompt puoi creare un'esperienza di
installazione personalizzata con un pulsante dedicato e un design coerente con la tua app.
// pwa-install.component.ts
import {
Component, signal, inject, PLATFORM_ID, afterNextRender
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: 'app-pwa-install',
standalone: true,
template: `
<div class="install-banner">
<p>Installa la nostra app per un'esperienza migliore!</p>
<button (click)="installApp()">Installa</button>
<button (click)="dismissBanner()">Non ora</button>
</div>
`
})
export class PwaInstallComponent {
private platformId = inject(PLATFORM_ID);
showInstallBanner = signal(false);
isAppInstalled = signal(false);
private deferredPrompt: any = null;
constructor() {
afterNextRender(() => {
this.setupInstallPrompt();
this.checkIfInstalled();
});
}
private setupInstallPrompt(): void {
if (!isPlatformBrowser(this.platformId)) return;
// Intercetta l'evento beforeinstallprompt
window.addEventListener('beforeinstallprompt', (event: Event) => {
event.preventDefault();
this.deferredPrompt = event;
this.showInstallBanner.set(true);
});
// Rileva quando l'app viene installata
window.addEventListener('appinstalled', () => {
this.isAppInstalled.set(true);
this.showInstallBanner.set(false);
this.deferredPrompt = null;
console.log('PWA installata con successo!');
});
}
async installApp(): Promise<void> {
if (!this.deferredPrompt) return;
// Mostra il prompt di installazione nativo
this.deferredPrompt.prompt();
const result = await this.deferredPrompt.userChoice;
console.log('Scelta utente:', result.outcome);
if (result.outcome === 'accepted') {
console.log('Installazione accettata');
} else {
console.log('Installazione rifiutata');
}
this.deferredPrompt = null;
this.showInstallBanner.set(false);
}
dismissBanner(): void {
this.showInstallBanner.set(false);
}
private checkIfInstalled(): void {
// Verifica se l'app e già in modalità standalone
const isStandalone = window.matchMedia(
'(display-mode: standalone)'
).matches;
this.isAppInstalled.set(isStandalone);
}
}
Criteri di Installabilita PWA
| Requisito | Descrizione |
|---|---|
| HTTPS | L'app deve essere servita su HTTPS (o localhost per dev) |
| Web App Manifest | Deve includere name, icons (192px + 512px), start_url, display |
| Service Worker | Un service worker attivo con un event handler fetch |
| Non già installata | L'app non deve essere già installata sul dispositivo |
| Interazione utente | L'utente deve aver interagito con il dominio (clic, scroll, ecc.) |
10. Testing e Debugging
Testare una PWA richiede strumenti specifici perchè il service worker è attivo solo
in produzione e le funzionalità PWA non sono disponibili durante lo sviluppo con
ng serve. Chrome DevTools e Lighthouse forniscono tutto il necessario
per verificare e debuggare ogni aspetto della tua PWA.
Build e Servizio Locale per Test
# 1. Build di produzione
ng build
# 2. Installa un server HTTP statico (una tantum)
npm install -g http-server
# 3. Servi la build di produzione
cd dist/tua-app/browser
http-server -p 8080 -c-1
# Oppure con npx (senza installazione globale)
npx serve dist/tua-app/browser -l 8080
# 4. Apri il browser su http://localhost:8080
# 5. Apri DevTools > Application tab
Chrome DevTools - Application Tab
La tab Application di Chrome DevTools è il pannello principale per ispezionare e debuggare tutti gli aspetti della PWA.
Pannelli DevTools per PWA
| Pannello | Funzione | Verifica |
|---|---|---|
| Manifest | Mostra il manifest parsed | Nome, icone, display mode, start URL sono corretti |
| Service Workers | Stato dei SW registrati | SW attivo, aggiornamenti in attesa, errori |
| Cache Storage | Contenuto delle cache | Le risorse giuste sono in cache, dimensioni totali |
| IndexedDB | Database locali | Dati offline persistiti correttamente |
| Storage | Quota e utilizzo storage | Spazio usato vs disponibile |
Lighthouse PWA Audit
Lighthouse include una sezione dedicata alla validazione PWA. L'audit verifica i criteri di installabilita, le ottimizzazioni per offline e le best practice. Una PWA ben configurata dovrebbe ottenere un punteggio Lighthouse PWA di 100/100.
Checklist Lighthouse PWA
- Il service worker registra un handler
fetch - La pagina risponde con un codice 200 anche offline
start_urlnel manifest e raggiungibile offline- Il manifest ha
nameoshort_name - Il manifest ha icone di almeno 192px e 512px
- Il manifest specifica
display(standalone/fullscreen/minimal-ui) - Il contenuto ha dimensioni adeguate al viewport
- Meta tag
theme-colorpresente nell'HTML - La pagina ha un
viewportmeta tag valido - Il sito usa HTTPS
- HTTP viene reindirizzato a HTTPS
Debugging del Service Worker
// Nel browser, via DevTools Console:
// 1. Verificare lo stato del service worker
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(reg => {
console.log('SW Scope:', reg.scope);
console.log('SW State:', reg.active?.state);
console.log('SW Waiting:', reg.waiting);
console.log('SW Installing:', reg.installing);
});
});
// 2. Forzare l'aggiornamento del service worker
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(reg => reg.update());
});
// 3. Deregistrare tutti i service worker (per reset completo)
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(reg => reg.unregister());
});
// 4. Ispezionare la cache del service worker
caches.keys().then(names => {
names.forEach(name => {
caches.open(name).then(cache => {
cache.keys().then(requests => {
console.log(`Cache "${name}":`, requests.length, 'entries');
requests.forEach(req => console.log(' -', req.url));
});
});
});
});
// 5. Simulare stato offline via DevTools
// Network tab > seleziona "Offline" nel dropdown throttling
// Oppure: Application > Service Workers > spunta "Offline"
Problemi Comuni e Soluzioni
- SW non si aggiorna: Usa "Update on reload" in DevTools > Application > Service Workers durante lo sviluppo
- Cache vecchia persistente: Cancella lo storage completo da DevTools > Application > Storage > "Clear site data"
- Install prompt non appare: Verifica che tutti i criteri di installabilita siano soddisfatti nel pannello Manifest
- Push notification non arrivano: Controlla che il permesso sia "granted" con
Notification.permissione che la sottoscrizione sia valida - Errori CORS in cache: Le risorse esterne devono essere servite con header CORS appropriati per essere messe in cache dal SW
- SW in stato "waiting": Il nuovo SW aspetta che tutte le tab dell'app siano chiuse. Usa
skipWaiting()per forzare l'attivazione immediata
Riepilogo e Prossimi Passi
Le Progressive Web App rappresentano un'evoluzione naturale delle applicazioni web, offrendo funzionalità che fino a pochi anni fa erano esclusive delle app native. Angular semplifica enormemente la creazione di PWA grazie al supporto integrato per service worker, caching intelligente, push notification e aggiornamenti gestiti.
Concetti Chiave di Questo Articolo
- PWA: Applicazioni web installabili con supporto offline, push notification e aggiornamenti automatici
- Setup:
ng add @angular/pwagenera manifest, configurazione SW e icone - Manifest: File JSON che descrive l'app (nome, icone, display mode, start URL)
- ngsw-config.json: Configurazione degli assetGroups (risorse statiche) e dataGroups (API dinamiche)
- Caching: Strategie prefetch/lazy per asset, freshness/performance per dati API
- Offline: Rilevamento stato rete, IndexedDB per persistenza, sincronizzazione al ritorno online
- Push:
SwPushper sottoscrizioni e ricezione notifiche, VAPID per autenticazione server - Aggiornamenti:
SwUpdateper controllare, notificare e attivare nuove versioni - Install prompt:
beforeinstallpromptper UI di installazione personalizzata - Testing: Chrome DevTools Application tab, Lighthouse PWA audit, debugging SW
Nel prossimo articolo della serie esploreremo la Dependency Injection Avanzata
in Angular, analizzando injection token personalizzati, provider tree-shakable,
multi-provider, factory provider e le strategie di scoping con providedIn.
Domande frequenti
Come funziona il caching delle risorse statiche in una PWA creata con Angular?
Il caching delle risorse statiche viene gestito automaticamente dal service worker, che memorizza le risorse richieste nella cache per poterle servire offline.
Quali sono le strategie di caching supportate da Angular NGSW?
Le strategie di caching supportate da Angular NGSW sono freshness (network-first), performance (cache-first) e caching statico per gli asset. La strategia stale-while-revalidate non è nativamente supportata, ma può essere simulata combinando la strategia performance con un breve.
Come gestire lo stato di connessione in una PWA creata con Angular?
Per gestire lo stato di connessione, è possibile utilizzare il rilevamento dello stato di connessione tramite l'API Navigator.onLine. Inoltre, per un'esperienza offline completa, è necessario gestire anche le pagine di fallback e la persistenza dei dati locali.
Come creare un install prompt personalizzato in una PWA creata con Angular?
Per creare un install prompt personalizzato, è possibile intercettare l'evento beforeinstallprompt e mostrare un pulsante dedicato per l'installazione.

Commenti
Caricamento commenti...
Accedi per commentare