Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

DevOps pour les Service Workers Angular

Lorsque vous transformez votre application Angular en PWA, le service worker devient un élément central de votre infrastructure. Pour que tout fonctionne correctement en production, il est essentiel de suivre certaines bonnes pratiques DevOps. Ce tutoriel vous explique comment gérer le déploiement, les mises à jour, la sécurité et le monitoring de votre service worker.

Une analogie simple : la bibliothèque et son catalogue

Imaginez une bibliothèque qui utilise un système de catalogue pour gérer ses livres. Chaque fois qu'un nouveau livre arrive ou qu'un livre est retiré, le catalogue doit être mis à jour. Si le catalogue et les livres ne correspondent pas, les lecteurs risquent de chercher des livres qui n'existent plus ou de ne pas trouver les nouveaux livres disponibles.

Le service worker fonctionne de la même manière : il maintient un "catalogue" (le fichier ngsw.json) qui liste tous les fichiers de votre application. Si ce catalogue ne correspond pas aux fichiers réellement disponibles sur le serveur, votre application peut se comporter de manière inattendue, voire cesser de fonctionner.

1. Gérer correctement les mises à jour du service worker

Comprendre le processus de build

À chaque fois que vous exécutez ng build, Angular génère plusieurs éléments essentiels :

  • De nouveaux fichiers : les fichiers JavaScript, CSS, HTML compilés avec leurs noms de version
  • Un fichier de manifeste : ngsw.json qui décrit toute la version de l'application
  • Des empreintes (hashes) : des identifiants uniques pour chaque fichier qui garantissent leur intégrité

Ces éléments forment un ensemble cohérent qui représente une version complète de votre application.

Les bonnes pratiques de déploiement

Pour que le service worker fonctionne correctement, vous devez :

Déployer tous les fichiers du build sans exception

Ne laissez jamais d'anciens fichiers sur le serveur. Si vous déployez une nouvelle version, remplacez complètement l'ancienne.

Remplacer l'ancien dossier par le nouveau

Évitez de mélanger des fichiers de différentes versions. Le service worker doit avoir une vision claire et cohérente des fichiers de la version actuelle.

S'assurer que le serveur ne sert jamais d'anciens fichiers mélangés avec des nouveaux

Un déploiement partiel peut causer des incohérences. Par exemple, si le nouveau ngsw.json référence des fichiers qui n'ont pas encore été déployés, le service worker ne pourra pas les trouver.

Déploiement atomique

Un déploiement doit être atomique : soit tous les fichiers sont déployés, soit aucun. Utilisez des techniques comme le déploiement dans un nouveau dossier suivi d'un changement de symlink, ou des outils qui garantissent la cohérence.

Exemple de script de déploiement

Voici un exemple de script bash qui garantit un déploiement atomique :

bash
#!/bin/bash
# Build de l'application
ng build --configuration production

# Création d'un dossier avec timestamp
TIMESTAMP=$(date +%s)
NEW_DIR="dist_$TIMESTAMP"

# Copie du build dans le nouveau dossier
cp -r dist/my-app "$NEW_DIR"

# Changement atomique du symlink
ln -sfn "$NEW_DIR" /var/www/my-app/current

# Nettoyage des anciens dossiers (garder les 5 derniers)
ls -dt dist_* | tail -n +6 | xargs rm -rf

2. Permettre au service worker de détecter les nouvelles versions

Comment fonctionne la détection de version

Le service worker d'Angular fonctionne de manière intelligente :

  1. Il compare les fichiers actuels : à chaque chargement de l'application, il vérifie le fichier ngsw.json
  2. Il détecte s'il existe une nouvelle version : si les hashes des fichiers ont changé, une nouvelle version est disponible
  3. Il télécharge cette version en arrière-plan : sans interrompre l'utilisation de l'application
  4. Il applique proprement la nouvelle version : lors du prochain rechargement de la page

Déployer proprement vos builds

Pour que cette détection fonctionne :

Déployer proprement vos builds

Assurez-vous que chaque déploiement est complet et cohérent. Le fichier ngsw.json doit toujours correspondre aux fichiers présents sur le serveur.

Tester la mise à jour

Après chaque déploiement, testez que l'application détecte correctement la nouvelle version. Vous pouvez utiliser les DevTools Chrome :

  1. Ouvrez les DevTools (F12)
  2. Allez dans l'onglet Application
  3. Cliquez sur Service Workers
  4. Vérifiez l'état du service worker et forcez une mise à jour si nécessaire

Notifier l'utilisateur d'une nouvelle version

Bien que le service worker gère les mises à jour automatiquement, il est souvent préférable d'informer l'utilisateur qu'une nouvelle version est disponible et lui proposer de recharger la page.

Voici comment implémenter cette fonctionnalité :

typescript
import { Component, inject, signal } from '@angular/core';
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { filter } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    @if (updateAvailable()) {
      <div class="update-banner">
        <p>Une nouvelle version de l'application est disponible.</p>
        <button (click)="reloadPage()">Recharger</button>
      </div>
    }
    <!-- Reste de votre application -->
  `
})
export class AppComponent {
  // Injection du service SwUpdate pour gérer les mises à jour
  private swUpdate = inject(SwUpdate);
  
  // Signal qui indique si une mise à jour est disponible
  // Utilisation d'un signal pour une gestion réactive de l'état
  updateAvailable = signal(false);

  constructor() {
    // Vérifier si le service worker est activé
    if (this.swUpdate.isEnabled) {
      // Écouter les événements de version prête
      // takeUntilDestroyed() gère automatiquement le nettoyage à la destruction
      this.swUpdate.versionUpdates
        .pipe(
          filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'),
          takeUntilDestroyed()
        )
        .subscribe(() => {
          // Afficher la notification à l'utilisateur en mettant à jour le signal
          this.updateAvailable.set(true);
        });

      // Vérifier périodiquement les mises à jour
      this.swUpdate.checkForUpdate();
    }
  }

  // Recharger la page pour activer la nouvelle version
  reloadPage() {
    window.location.reload();
  }
}

Vérification périodique

Vous pouvez également vérifier périodiquement les mises à jour en ajoutant un intervalle dans le constructeur :

typescript
constructor() {
  // ... code existant ...
  
  // Vérifier périodiquement les mises à jour toutes les minutes
  if (this.swUpdate.isEnabled) {
    setInterval(() => {
      this.swUpdate.checkForUpdate();
    }, 60000);
  }
}

3. S'assurer que les fichiers ne sont pas corrompus

Le système de vérification d'intégrité

Le service worker d'Angular utilise un système de hashes (empreintes) pour vérifier l'intégrité des fichiers. Chaque fichier a un hash unique calculé à partir de son contenu. Si le contenu change, le hash change aussi.

Lorsque le service worker télécharge un fichier, il compare le hash du fichier téléchargé avec le hash attendu dans ngsw.json. Si les hashes ne correspondent pas, le fichier est considéré comme corrompu.

Les bonnes pratiques de sécurité

Héberger l'application sous HTTPS

Les service workers nécessitent HTTPS pour fonctionner (sauf en localhost pour le développement). HTTPS garantit que les fichiers ne sont pas modifiés en transit entre le serveur et le navigateur.

Éviter les modifications manuelles

Ne modifiez jamais manuellement les fichiers dans le dossier dist après le build. Si vous devez modifier un fichier, modifiez le code source et relancez le build.

Éviter que les proxys ou CDNs modifient les fichiers

Certains proxys ou CDNs peuvent modifier les fichiers (compression, minification supplémentaire, injection de code...). Ces modifications changent les hashes et peuvent causer des problèmes.

Si vous utilisez un CDN ou un proxy, configurez-le pour qu'il ne modifie pas les fichiers Angular, ou désactivez la mise en cache pour les fichiers critiques.

Garder des fichiers versionnés cohérents

Un build partiellement déployé peut causer des incohérences. Par exemple, si ngsw.json référence un fichier avec un hash spécifique, mais que ce fichier n'a pas été déployé ou a été modifié, le service worker détectera une corruption.

Comportement en cas de corruption détectée

Si Angular détecte un fichier corrompu :

  1. Il désactive le cache : le service worker cesse d'utiliser le cache corrompu
  2. Il repasse en mode "normal" : l'application fonctionne comme une application web classique, sans les avantages du service worker
  3. Il protège l'utilisateur : plutôt que de servir du contenu potentiellement incorrect, Angular préfère désactiver le service worker

Désactivation automatique

Si le service worker se désactive automatiquement, c'est un signe qu'il y a un problème avec votre déploiement ou votre infrastructure. Vérifiez vos logs et votre processus de déploiement.

4. Gérer les erreurs et prévoir un "plan B"

Les problèmes courants

Un service worker peut rencontrer plusieurs types de problèmes :

  • Cache corrompu : les données en cache sont invalides ou incohérentes
  • Fichier illisible : un fichier référencé dans ngsw.json n'existe pas ou n'est pas accessible
  • Version incohérente : les fichiers déployés ne correspondent pas à ngsw.json
  • Déploiement incomplet : certains fichiers n'ont pas été déployés

Les bonnes pratiques de gestion d'erreurs

Ne jamais modifier le fichier ngsw.json

Le fichier ngsw.json est généré automatiquement par Angular lors du build. Ne le modifiez jamais manuellement. Toute modification peut causer des incohérences et des erreurs.

Surveiller l'enregistrement du service worker

Lors de chaque déploiement, vérifiez que le service worker s'enregistre correctement. Vous pouvez le faire via les DevTools ou en ajoutant des logs dans votre application :

typescript
import { SwUpdate } from '@angular/service-worker';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

// Dans votre composant ou service
constructor() {
  if (this.swUpdate.isEnabled) {
    // Écoute l'activation du service worker
    // takeUntilDestroyed() gère automatiquement le nettoyage
    this.swUpdate.activated
      .pipe(takeUntilDestroyed())
      .subscribe(() => {
        console.log('Service worker activé avec succès');
      });

    // Écoute les nouvelles versions disponibles
    this.swUpdate.available
      .pipe(takeUntilDestroyed())
      .subscribe(() => {
        console.log('Nouvelle version disponible');
      });
  }
}

Vider les caches en cas de problème

Si vous suspectez un problème de cache :

  1. Pour les développeurs : utilisez les DevTools Chrome → Application → Storage → Clear site data
  2. Pour les utilisateurs : guidez-les pour vider le cache de leur navigateur, ou implémentez une fonctionnalité dans votre application pour réinitialiser le service worker

Redéployer proprement l'application

Si un cache est endommagé ou si vous avez détecté une incohérence, la meilleure solution est de redéployer proprement l'application. Un nouveau build et un nouveau déploiement complet résoudront généralement les problèmes.

Implémentation d'un mécanisme de récupération

Vous pouvez implémenter un mécanisme qui permet de réinitialiser le service worker en cas de problème :

typescript
import { Injectable, inject } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Injectable({
  providedIn: 'root'
})
export class ServiceWorkerService {
  private swUpdate = inject(SwUpdate);

  /**
   * Réinitialise le service worker en désenregistrant l'ancien
   * et en forçant un rechargement complet
   */
  async resetServiceWorker(): Promise<void> {
    if ('serviceWorker' in navigator) {
      const registrations = await navigator.serviceWorker.getRegistrations();
      
      // Désenregistrer tous les service workers
      for (const registration of registrations) {
        await registration.unregister();
      }

      // Vider tous les caches
      if ('caches' in window) {
        const cacheNames = await caches.keys();
        await Promise.all(
          cacheNames.map(cacheName => caches.delete(cacheName))
        );
      }

      // Recharger la page pour réenregistrer le service worker
      window.location.reload();
    }
  }
}

5. Vérifier l'état du service worker

Les outils de monitoring

Angular fournit plusieurs moyens de vérifier l'état du service worker :

Les DevTools Chrome

  1. Ouvrez les DevTools (F12)
  2. Allez dans l'onglet Application
  3. Cliquez sur Service Workers dans le menu de gauche
  4. Vous verrez l'état du service worker, sa version, et vous pourrez le mettre à jour ou le désactiver

Les outils internes d'Angular

Angular expose des endpoints spéciaux pour le debugging (si activés) :

  • /ngsw/state : affiche l'état actuel du service worker
  • /ngsw/state.json : retourne l'état au format JSON

Pour activer ces endpoints en développement, vous pouvez ajouter cette configuration dans votre app.config.ts :

typescript
import { provideServiceWorker } from '@angular/service-worker';
import { isDevMode } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    // ... autres providers
    provideServiceWorker('ngsw-worker.js', {
      enabled: !isDevMode(),
      registrationStrategy: 'registerWhenStable:30000'
    })
  ]
};

Ce qu'il faut vérifier à chaque déploiement

Lors de chaque déploiement, vérifiez que :

Le service worker s'enregistre correctement

Ouvrez les DevTools et vérifiez que le service worker apparaît dans la liste et qu'il est actif.

Les caches se créent

Dans les DevTools → Application → Cache Storage, vérifiez que les caches Angular sont créés et contiennent les fichiers attendus.

L'application fonctionne hors ligne

  1. Activez le mode offline dans les DevTools (Network → Offline)
  2. Rechargez la page
  3. Vérifiez que l'application fonctionne toujours

Exemple de composant de monitoring

Vous pouvez créer un composant de monitoring pour afficher l'état du service worker dans votre application :

typescript
import { Component, inject, signal } from '@angular/core';
import { SwUpdate, VersionEvent } from '@angular/service-worker';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-sw-status',
  standalone: true,
  template: `
    <div class="sw-status">
      <h3>État du Service Worker</h3>
      <p>Activé : {{ isEnabled() ? 'Oui' : 'Non' }}</p>
      <p>Version actuelle : {{ currentVersion() }}</p>
      <p>Nouvelle version disponible : {{ updateAvailable() ? 'Oui' : 'Non' }}</p>
      @if (updateAvailable()) {
        <button (click)="activateUpdate()">Activer la mise à jour</button>
      }
    </div>
  `
})
export class ServiceWorkerStatusComponent {
  private swUpdate = inject(SwUpdate);
  
  // Signal qui indique si le service worker est activé
  // Cette valeur est statique et peut être définie directement
  isEnabled = signal(this.swUpdate.isEnabled);
  
  // Signal pour stocker la version actuelle
  currentVersion = signal<string>('Inconnue');
  
  // Signal qui indique si une mise à jour est disponible
  updateAvailable = signal(false);

  constructor() {
    if (this.isEnabled()) {
      // Écoute les événements de version
      // takeUntilDestroyed() gère automatiquement le nettoyage
      this.swUpdate.versionUpdates
        .pipe(takeUntilDestroyed())
        .subscribe((event: VersionEvent) => {
          if (event.type === 'VERSION_READY') {
            // Met à jour les signaux avec les nouvelles informations
            this.updateAvailable.set(true);
            this.currentVersion.set(event.latestVersion.hash);
          }
        });

      // Écoute l'activation pour récupérer la version actuelle
      // On peut utiliser toSignal pour obtenir la dernière valeur activée
      this.swUpdate.activated
        .pipe(takeUntilDestroyed())
        .subscribe((update) => {
          if (update?.current) {
            this.currentVersion.set(update.current.hash);
          }
        });
    }
  }

  // Activer la nouvelle version
  async activateUpdate() {
    if (this.swUpdate.isEnabled) {
      await this.swUpdate.activateUpdate();
      window.location.reload();
    }
  }
}

6. Bien préparer l'environnement de déploiement

Les exigences techniques

Pour que votre PWA fonctionne correctement, votre environnement de déploiement doit respecter certaines exigences :

Servir l'application sous HTTPS

Les service workers nécessitent HTTPS (sauf en localhost). Assurez-vous que votre serveur est configuré avec un certificat SSL valide.

Servir correctement les fichiers statiques

Votre serveur doit être capable de servir tous les fichiers statiques (JS, CSS, images, etc.) avec les bons headers HTTP. Les fichiers doivent être accessibles directement via leur URL.

Éviter la réécriture ou la compression incorrecte

Certains serveurs ou proxys peuvent réécrire ou compresser les fichiers. Cela peut modifier les hashes et causer des problèmes. Configurez votre serveur pour qu'il ne modifie pas les fichiers Angular.

Configurer le fallback vers index.html

Pour les routes Angular (routing côté client), votre serveur doit retourner index.html pour toutes les routes qui ne correspondent pas à des fichiers statiques. C'est ce qu'on appelle le "fallback".

Exemple de configuration serveur

Voici des exemples de configuration pour différents serveurs :

Nginx

nginx
server {
    listen 80;
    server_name example.com;
    root /var/www/my-app;
    index index.html;

    # Servir les fichiers statiques
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Headers pour les fichiers statiques
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Headers spécifiques pour ngsw.json (ne pas mettre en cache)
    location = /ngsw.json {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
    }
}

Apache (.htaccess)

apache
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.html$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.html [L]
</IfModule>

# Headers pour ngsw.json
<FilesMatch "ngsw\.json$">
    Header set Cache-Control "no-cache, no-store, must-revalidate"
</FilesMatch>

Node.js avec Express

typescript
import express from 'express';
import path from 'path';

const app = express();
const distPath = path.join(__dirname, 'dist/my-app');

// Servir les fichiers statiques
app.use(express.static(distPath));

// Fallback vers index.html pour les routes Angular
app.get('*', (req, res) => {
  res.sendFile(path.join(distPath, 'index.html'));
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Headers HTTP importants

Assurez-vous que votre serveur envoie les bons headers HTTP :

  • Content-Type correct pour chaque type de fichier
  • Cache-Control approprié (les fichiers Angular peuvent être mis en cache longtemps, mais ngsw.json ne doit pas être mis en cache)

Déploiement propre et cohérent

Le point le plus important : chaque déploiement doit être propre, complet et cohérent.

Cela signifie :

  • Tous les fichiers sont déployés en même temps
  • Aucun fichier de l'ancienne version ne reste sur le serveur
  • Le fichier ngsw.json correspond exactement aux fichiers présents
  • Le déploiement est atomique (tout ou rien)

7. Comprendre la logique du cache

Comment fonctionne le cache Angular

Le service worker d'Angular met en cache différents types de ressources :

Les fichiers critiques

Les fichiers JavaScript, CSS, HTML de votre application sont automatiquement mis en cache. Ces fichiers sont essentiels au fonctionnement de l'application.

Les assets selon la configuration

Les autres assets (images, polices, etc.) sont mis en cache selon la configuration dans ngsw-config.json. Vous pouvez spécifier quels fichiers mettre en cache et comment.

Configuration du cache

Le fichier ngsw-config.json vous permet de configurer le comportement du cache :

json
{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js"
        ]
      }
    },
    {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2)"
        ]
      }
    }
  ]
}

Les bonnes pratiques de cache

Mettre en cache uniquement ce qui est utile

Ne mettez pas en cache des fichiers qui changent fréquemment ou qui ne sont pas nécessaires hors ligne. Cela gaspille de l'espace de stockage et peut causer des problèmes de synchronisation.

Éviter de mettre en cache des fichiers dynamiques

Les fichiers générés dynamiquement (comme les images uploadées par les utilisateurs, les données API) ne doivent généralement pas être mis en cache par le service worker. Utilisez plutôt le cache HTTP standard ou des stratégies spécifiques.

Tester les comportements hors ligne

Après chaque déploiement, testez que l'application fonctionne correctement hors ligne :

  1. Chargez l'application une fois en ligne
  2. Activez le mode offline dans les DevTools
  3. Rechargez la page
  4. Vérifiez que l'application fonctionne toujours