Salta al contenuto principale

8 - Angular + PWA: Creare un\

Trasforma la tua app Angular in una PWA: service worker, manifest, caching strategies, supporto offline, push notifications e prompt di installazione.

Cosa ne pensi di questo articolo?

8 - Angular + PWA: Creare un\
Angular ModernoFoto:Lukas BlazekPexels

In Breve

Trasforma la tua app Angular in una PWA: service worker, manifest, caching strategies, supporto offline, push notifications e prompt di installazione.

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
1Angular SignalsReattività fine-grained
2Zoneless Change DetectionEliminare Zone.js
3Nuovi Template @if, @for, @deferControl flow moderno
4Standalone ComponentsArchitettura senza NgModule
5Signal FormsForm reattivi con Signals
6SSR e Incremental HydrationServer-Side Rendering
7Core Web Vitals in AngularPerformance e metriche
8Sei qui - Angular PWAProgressive Web App
9Dependency Injection AvanzataDI tree-shakable
10Migrare da Angular 17 a 21Guida 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 il supporto PWA a un progetto Angular

# 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 con Service Worker registrato

// 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.

manifest.webmanifest completo e ottimizzato

{
  "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.

ngsw-config.json con configurazione completa

{
  "$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

Servizio per monitorare lo stato online/offline

// 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.

Servizio IndexedDB per persistenza offline

// 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 isPlatformBrowser prima 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

Servizio per gestire le push notification

// 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 Node.js per invio push notification (web-push)

// 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.

Servizio per gestire gli aggiornamenti dell'app

// 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.

Componente per gestire l'install prompt personalizzato

// 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

Comandi per testare la PWA localmente

# 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_url nel manifest e raggiungibile offline
  • Il manifest ha name o short_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-color presente nell'HTML
  • La pagina ha un viewport meta tag valido
  • Il sito usa HTTPS
  • HTTP viene reindirizzato a HTTPS

Debugging del Service Worker

Tecniche di debugging per Service Worker in Chrome

// 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.permission e 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/pwa genera 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: SwPush per sottoscrizioni e ricezione notifiche, VAPID per autenticazione server
  • Aggiornamenti: SwUpdate per controllare, notificare e attivare nuove versioni
  • Install prompt: beforeinstallprompt per 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.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti