Appearance
Service Worker Personnalisé dans Angular
Dans ce tutoriel, nous allons découvrir comment créer et personnaliser un service worker dans Angular. Vous apprendrez à étendre le service worker par défaut d'Angular pour ajouter des fonctionnalités avancées comme les notifications push, la synchronisation en arrière-plan, et des logiques personnalisées pour intercepter les requêtes réseau.
Qu'est-ce qu'un service worker personnalisé ?
Imaginez que vous utilisez une application mobile comme WhatsApp. Cette application peut vous envoyer des notifications même quand elle n'est pas ouverte, synchroniser vos messages en arrière-plan, et fonctionner même sans connexion Internet. Un service worker personnalisé permet d'ajouter ces mêmes fonctionnalités à votre application web Angular.
Par défaut, Angular fournit un service worker "clé en main" (ngsw-worker.js) qui gère le cache des fichiers, le mode hors ligne basique, et les mises à jour automatiques. C'est très pratique pour commencer, mais il a des limites : il ne gère que le caching statique des fichiers selon votre configuration.
Un service worker personnalisé est un service worker que vous écrivez vous-même, en plus (ou à la place) de celui fourni par Angular. Cela vous permet d'ajouter des comportements plus avancés que le simple cache.
Pourquoi créer un service worker personnalisé ?
Le service worker standard d'Angular est excellent pour le cache et le mode offline basique, mais il ne suffit pas si vous avez besoin de :
- Notifications push : Envoyer des notifications à l'utilisateur même quand l'application n'est pas ouverte
- Synchronisation en arrière-plan : Synchroniser des données quand l'utilisateur retrouve le réseau après avoir été hors ligne
- Logiques personnalisées : Intercepter toutes les requêtes réseau, rediriger vers des pages offline personnalisées, gérer des caches dynamiques, etc.
- Combiner le meilleur des deux mondes : Garder le cache/offline/mise à jour automatique d'Angular, tout en ajoutant vos fonctionnalités personnalisées
En bref, un service worker personnalisé est utile dès que votre application a besoin de plus que du simple caching statique.
Comment fonctionne un service worker personnalisé ?
Un service worker agit comme un "pont" entre votre application et le réseau. Il peut intercepter toutes les requêtes réseau, gérer des caches, recevoir des événements (comme les notifications push), et exécuter du code même quand l'application n'est pas ouverte.
Quand vous créez un service worker personnalisé, vous pouvez :
- Importer le service worker d'Angular pour garder toutes ses fonctionnalités (cache, offline, mises à jour)
- Ajouter votre propre logique par-dessus pour gérer les notifications, la synchronisation, etc.
C'est comme si vous héritiez de toutes les fonctionnalités d'Angular et que vous y ajoutiez les vôtres.
Créer un service worker personnalisé
Voyons comment créer un service worker personnalisé étape par étape.
Étape 1 : Créer le fichier du service worker
Créez un fichier custom-sw.js dans le dossier src/ de votre projet. Ce fichier contiendra votre logique personnalisée.
Voici un exemple minimal :
typescript
// custom-sw.js
// Importe le service worker d'Angular pour garder toutes ses fonctionnalités
importScripts('./ngsw-worker.js');
(function () {
'use strict';
// Gère le clic sur une notification
// Quand l'utilisateur clique sur une notification, on ouvre l'application
// à une URL spécifique si elle est fournie dans les données de la notification
self.addEventListener('notificationclick', (event) => {
console.log('Notification click reçue', event.notification);
// Ferme la notification
event.notification.close();
// Ouvre l'application à l'URL spécifiée dans les données de la notification
if (clients.openWindow && event.notification.data?.url) {
event.waitUntil(clients.openWindow(event.notification.data.url));
}
});
// Gère la synchronisation en arrière-plan
// Quand l'utilisateur retrouve le réseau, on peut synchroniser des données
// qui ont été mises en attente pendant qu'il était hors ligne
self.addEventListener('sync', (event) => {
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
});
// Fonction qui effectue la synchronisation en arrière-plan
// Elle envoie les données en attente vers le serveur
function doBackgroundSync() {
return fetch('https://api.exemple.com/sync')
.then(res => res.json())
.then(data => {
console.log('Sync OK', data);
// Ici, vous pouvez mettre à jour le cache local ou notifier l'utilisateur
})
.catch(err => {
console.error('Sync échouée', err);
// En cas d'erreur, on peut réessayer plus tard
});
}
// Intercepte toutes les requêtes réseau pour ajouter une logique personnalisée
// Par exemple, on peut rediriger certaines requêtes vers le cache ou ajouter des headers
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Exemple : rediriger vers une page offline personnalisée pour certaines routes
if (url.pathname.startsWith('/api/') && event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => {
// Si la requête échoue (hors ligne), on peut retourner une réponse personnalisée
return new Response(
JSON.stringify({ error: 'Hors ligne', message: 'Cette fonctionnalité nécessite une connexion Internet' }),
{
headers: { 'Content-Type': 'application/json' },
status: 503
}
);
})
);
}
});
})();Analysons ce code :
importScripts('./ngsw-worker.js'): Cette ligne est cruciale. Elle importe le service worker d'Angular, ce qui vous permet de garder toutes ses fonctionnalités (cache, offline, mises à jour automatiques) tout en ajoutant les vôtres.IIFE (Immediately Invoked Function Expression) : Le code est enveloppé dans une fonction auto-exécutée
(function() { ... })()pour éviter de polluer le scope global.Événement
notificationclick: Gère le clic sur une notification. Quand l'utilisateur clique, on peut ouvrir l'application à une URL spécifique.Événement
sync: Permet la synchronisation en arrière-plan. Quand l'utilisateur retrouve le réseau, le service worker peut automatiquement synchroniser des données en attente.Événement
fetch: Intercepte toutes les requêtes réseau. Vous pouvez ajouter une logique personnalisée, comme rediriger vers le cache ou retourner des réponses personnalisées.
Important
Si vous oubliez d'importer ngsw-worker.js dans votre service worker personnalisé, vous perdrez toutes les fonctionnalités de cache, offline et mise à jour automatique fournies par Angular. Vous vous retrouverez avec un service worker "maison" sans les fonctionnalités de base.
Étape 2 : Ajouter le fichier aux assets
Pour que le fichier soit inclus dans le build, vous devez l'ajouter à la liste des assets dans angular.json :
json
{
"projects": {
"votre-projet": {
"architect": {
"build": {
"options": {
"assets": [
"src/favicon.ico",
"src/assets",
"src/custom-sw.js"
]
}
}
}
}
}
}Cela garantit que custom-sw.js sera copié dans le dossier de build et accessible depuis la racine de l'application.
Étape 3 : Configurer Angular pour utiliser le service worker personnalisé
Modifiez votre fichier app.config.ts pour indiquer à Angular d'utiliser votre service worker personnalisé au lieu du service worker par défaut :
typescript
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideServiceWorker } from '@angular/service-worker';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
// Enregistre le service worker personnalisé au lieu du service worker par défaut
// Le service worker est activé uniquement en production
provideServiceWorker('custom-sw.js', {
enabled: !isDevMode(),
registrationStrategy: 'registerWhenStable:30000'
})
]
};La différence avec la configuration standard est que nous spécifions 'custom-sw.js' au lieu de 'ngsw-worker.js'. Ainsi, le navigateur enregistrera votre service worker personnalisé.
Étape 4 : Utiliser les fonctionnalités dans votre application
Maintenant que votre service worker personnalisé est en place, vous pouvez utiliser ses fonctionnalités dans votre application Angular.
Exemple : Envoyer une notification
Voici comment envoyer une notification depuis votre application :
typescript
import { Component, inject } from '@angular/core';
@Component({
selector: 'app-user',
standalone: true,
template: `
<div>
<h1>Gestion des utilisateurs</h1>
<button (click)="sendNotification()">
Envoyer une notification
</button>
</div>
`
})
export class UserComponent {
// Envoie une notification via le service worker
// Cette méthode demande la permission à l'utilisateur, puis envoie une notification
sendNotification() {
if ('Notification' in window && 'serviceWorker' in navigator) {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
navigator.serviceWorker.ready.then(registration => {
registration.showNotification('Nouvel utilisateur', {
body: 'Un nouvel utilisateur a été ajouté',
icon: '/assets/icon-192x192.png',
badge: '/assets/badge.png',
tag: 'user-notification',
data: {
url: '/users'
}
});
});
}
});
}
}
}Exemple : Synchronisation en arrière-plan
Voici comment déclencher une synchronisation en arrière-plan :
typescript
import { Component, inject } from '@angular/core';
import { UserService } from '../core/services/user.service';
@Component({
selector: 'app-user-form',
standalone: true,
template: `
<form (ngSubmit)="saveUser()">
<input [(ngModel)]="userName" placeholder="Nom d'utilisateur" />
<button type="submit">Enregistrer</button>
</form>
`
})
export class UserFormComponent {
private userService = inject(UserService);
userName = '';
// Enregistre un utilisateur et déclenche une synchronisation en arrière-plan
// Si l'utilisateur est hors ligne, les données seront synchronisées quand il retrouvera le réseau
async saveUser() {
try {
await this.userService.createUser({ name: this.userName });
} catch (error) {
// Si la requête échoue (hors ligne), on enregistre les données localement
// et on déclenche une synchronisation en arrière-plan
if ('serviceWorker' in navigator && 'sync' in (ServiceWorkerRegistration.prototype as any)) {
const registration = await navigator.serviceWorker.ready;
// Enregistre les données localement (dans IndexedDB par exemple)
await this.saveUserLocally({ name: this.userName });
// Demande une synchronisation en arrière-plan
await (registration as any).sync.register('background-sync');
}
}
}
// Sauvegarde les données localement pour les synchroniser plus tard
private async saveUserLocally(user: { name: string }) {
// Ici, vous pouvez utiliser IndexedDB ou le cache du service worker
// pour stocker les données en attente
console.log('Utilisateur sauvegardé localement:', user);
}
}Bonnes pratiques et précautions
Gérer les erreurs
Le code du service worker s'exécute en arrière-plan, dans un contexte séparé. Si une erreur se produit dans votre service worker personnalisé, cela peut compromettre le fonctionnement complet du service worker (cache, offline, etc.). Il est donc crucial de bien gérer les erreurs :
typescript
self.addEventListener('sync', (event) => {
if (event.tag === 'background-sync') {
event.waitUntil(
doBackgroundSync()
.catch(err => {
console.error('Erreur lors de la synchronisation:', err);
// Ne pas rejeter l'événement pour éviter de bloquer les futures synchronisations
})
);
}
});Utiliser event.waitUntil()
Pour les opérations asynchrones dans les événements du service worker, utilisez toujours event.waitUntil() pour garantir que le service worker reste actif pendant l'exécution :
typescript
self.addEventListener('notificationclick', (event) => {
// Utilise waitUntil pour garder le service worker actif
event.waitUntil(
clients.openWindow('/users')
.then(() => {
console.log('Fenêtre ouverte avec succès');
})
.catch(err => {
console.error('Erreur lors de l\'ouverture de la fenêtre:', err);
})
);
});Tester soigneusement
Testez votre service worker personnalisé dans différents environnements :
- Mode production : Le service worker n'est activé qu'en production
- En ligne et hors ligne : Vérifiez que le cache d'Angular fonctionne toujours
- Réseau lent : Testez le comportement avec une connexion dégradée
- Différents navigateurs : Certaines fonctionnalités peuvent varier selon le navigateur
Bonne pratique
Utilisez les outils de développement du navigateur (onglet "Application" dans Chrome) pour inspecter votre service worker, voir son état, et tester différentes situations (offline, réseau lent, etc.).
Limitations du service worker
Rappelez-vous que le service worker :
- S'exécute dans un contexte séparé : Il n'a pas accès au DOM ni aux variables de votre application Angular. Il ne peut communiquer qu'à travers des APIs spécifiques (caches, fetch, postMessage/clients, notifications, etc.)
- Nécessite HTTPS : Le service worker ne fonctionne que sur HTTPS (sauf en local avec
localhost) - Peut être arrêté : Le navigateur peut arrêter le service worker à tout moment pour économiser les ressources
