Skip to main content

Service workers

Les service workers agissent comme des serveurs proxy qui gèrent les requêtes réseau au sein de votre application. Cela rend possible le fonctionnement hors-ligne de votre application, mais même si vous n’avez pas besoin de support hors-ligne (ou si vous ne pouvez pas réalistiquement l’implémenter à cause du type d’application que vous développez), il est souvent utile d’utiliser les service workers pour accélérer la navigation en pré-cachant vos JS et CSS compilés.

Dans SvelteKit, si vous avez un fichier src/service-worker.js (ou src/service-worker/index.js), celui-ci sera compilé et automatiquement activé.

Au sein du service worker

Au sein du service worker vous avez accès au module $service-worker, qui vous fournit les chemins vers tous les assets statiques, fichiers compilés et pages prérendues. Vous avez également accès à la chaîne de caractères représentant la version de l’application, que vous pouvez utiliser pour créer un nom de cache unique, ainsi qu’à la base du chemin de déploiement. Si votre configuration Vite précise l’option define (utilisé pour le remplacement de variables globales), celle-ci sera appliquée aux service workers ainsi qu’à vos fichiers compilés de serveur et de client.

L’exemple suivant met en cache l’application compilée et tout fichier dans le dossier static de manière agressive, et met en cache toute autre requête lorsqu’elles se produisent. Ceci rend de facto toute page visitée disponible en mode hors-ligne.

src/service-worker
// Désactive l'accès au types du DOM comme `HTMLElement` qui ne sont pas disponibles
// dans un service worker et instancie les variables globales pertinentes
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />

// Assure que l'import de `$service-worker` soit correctement typé
/// <reference types="@sveltejs/kit" />

// Uniquement nécessaire si vous avez un import depuis `$env/static/public`
/// <reference types="../.svelte-kit/ambient.d.ts" />

import { build, files, version } from '$service-worker';

// Ceci donne à `self` le typage correct
const self = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (globalThis.self));

// Crée un nom de cache unique pour ce déploiement
const CACHE = `cache-${version}`;

const ASSETS = [
	...build, // l'application elle-même
	...files  // tout ce qui est dans `static`
];

self.addEventListener('install', (event) => {
	// Crée un nouveau cache et y ajoute tous les fichiers
	async function addFilesToCache() {
		const cache = await caches.open(CACHE);
		await cache.addAll(ASSETS);
	}

	event.waitUntil(addFilesToCache());
});

self.addEventListener('activate', (event) => {
	// Supprime du disque les données précédemment mises en cache
	async function deleteOldCaches() {
		for (const key of await caches.keys()) {
			if (key !== CACHE) await caches.delete(key);
		}
	}

	event.waitUntil(deleteOldCaches());
});

self.addEventListener('fetch', (event) => {
	// ignore les requêtes POST, etc
	if (event.request.method !== 'GET') return;

	async function respond() {
		const url = new URL(event.request.url);
		const cache = await caches.open(CACHE);

		// les fichiers `build`/`files` peuvent toujours être servis depuis le cache
		if (ASSETS.includes(url.pathname)) {
			const response = await cache.match(url.pathname);

			if (response) {
				return response;
			}
		}

		// pour tout le reste, essayer d'abord le réseau, mais
		// se reporter au cache si le réseau est indisponible
		try {
			const response = await fetch(event.request);

			// si nous sommes hors-ligne, fetch peut renvoyer une valeur qui n'est pas une Response
			// plutôt que de lever une erreur — and nous ne pouvons pas passer cette non-Response à
			// respondWith
			if (!(response instanceof Response)) {
				throw new Error('la réponse de fetch est invalide');
			}

			if (response.status === 200 && !response.headers.get('cache-control')?.includes('no-store')) {
				cache.put(event.request, response.clone());
			}

			return response;
		} catch (err) {
			const response = await cache.match(event.request);

			if (response) {
				return response;
			}

			// s'il n'y a pas de cache, lever une erreur
			// puisqu'il n'y a rien à répondre à cette requête
			throw err;
		}
	}

	event.respondWith(respond());
});
// Désactive l'accès au types du DOM comme `HTMLElement` qui ne sont pas disponibles
// dans un service worker et instancie les variables globales pertinentes
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />

// Assure que l'import de `$service-worker` soit correctement typé
/// <reference types="@sveltejs/kit" />

// Uniquement nécessaire si vous avez un import depuis `$env/static/public`
/// <reference types="../.svelte-kit/ambient.d.ts" />

import { build, files, version } from '$service-worker';

// Ceci donne à `self` le typage correct
const self = globalThis.self as unknown as ServiceWorkerGlobalScope;

// Crée un nom de cache unique pour ce déploiement
const CACHE = `cache-${version}`;

const ASSETS = [
	...build, // l'application elle-même
	...files  // tout ce qui est dans `static`
];

self.addEventListener('install', (event) => {
	// Crée un nouveau cache et y ajoute tous les fichiers
	async function addFilesToCache() {
		const cache = await caches.open(CACHE);
		await cache.addAll(ASSETS);
	}

	event.waitUntil(addFilesToCache());
});

self.addEventListener('activate', (event) => {
	// Supprime du disque les données précédemment mises en cache
	async function deleteOldCaches() {
		for (const key of await caches.keys()) {
			if (key !== CACHE) await caches.delete(key);
		}
	}

	event.waitUntil(deleteOldCaches());
});

self.addEventListener('fetch', (event) => {
	// ignore les requêtes POST, etc
	if (event.request.method !== 'GET') return;

	async function respond() {
		const url = new URL(event.request.url);
		const cache = await caches.open(CACHE);

		// les fichiers `build`/`files` peuvent toujours être servis depuis le cache
		if (ASSETS.includes(url.pathname)) {
			const response = await cache.match(url.pathname);

			if (response) {
				return response;
			}
		}

		// pour tout le reste, essayer d'abord le réseau, mais
		// se reporter au cache si le réseau est indisponible
		try {
			const response = await fetch(event.request);

			// si nous sommes hors-ligne, fetch peut renvoyer une valeur qui n'est pas une Response
			// plutôt que de lever une erreur — and nous ne pouvons pas passer cette non-Response à
			// respondWith
			if (!(response instanceof Response)) {
				throw new Error('la réponse de fetch est invalide');
			}

			if (response.status === 200 && !response.headers.get('cache-control')?.includes('no-store')) {
				cache.put(event.request, response.clone());
			}

			return response;
		} catch (err) {
			const response = await cache.match(event.request);

			if (response) {
				return response;
			}

			// s'il n'y a pas de cache, lever une erreur
			// puisqu'il n'y a rien à répondre à cette requête
			throw err;
		}
	}

	event.respondWith(respond());
});

Faites attention lorsque vous mettez des choses en cache ! Dans certains cas, des données périmées peuvent être pires que pas de données du tout en mode hors-ligne. Puisque les navigateurs vont vider le cache s’il remplit trop, vous devriez également faire attention lorsque vous mettez en cache des assets de grande taille comme des fichiers vidéo.

build et prerendered sont des tableaux vides lors du développement.

Activation manuelle

Vous pouvez désactiver l’activation automatique si vous avez besoin de définir le service worker avec votre propre logique. La définition par défaut ressemble à quelque chose comme ça :

import { dev } from '$app/environment';

if ('serviceWorker' in navigator) {
	addEventListener('load', function () {
		navigator.serviceWorker.register('./path/to/service-worker.js', {
			type: dev ? 'module' : 'classic'
		});
	});
}

Le service worker est compilé pour la production, mais pas lors du développement.

Mettre à jour le service worker

Les navigateurs vérifient si un service worker à jour est disponible lorsqu’une navigation “de pleine page” se produit au sein de son scope, et après des évènements fonctionnels comme push et sync. Les clients côté client ne sont ni l’un ni l’autre, ce qui implique qu’une navigation au sein de votre application ne va en soi provoquer un nouveau déploiement de service worker.

SvelteKit exécute registration.update() uniquement en tant que partie de la gestion d’erreur — si un module de route échoue à se charger ou si une navigation résulte en un statut d’erreur, et que le polling de version détecte que l’application a été redéployée, le service worker sera mis à jour avant que SvelteKit ne déclenche une navigation complète pour se mettre à jour.

Si vous souhaitez que les nouveaux déploiements soient pris en compte de manière plus agressive, vous pouvez déclencher vous-même une vérification des mises à jour — par exemple pour chaque navigation côté client, dans votre layout racine :

import { afterNavigate } from '$app/navigation';

afterNavigate(async () => {
	if ('serviceWorker' in navigator) {
		const registration = await navigator.serviceWorker.getRegistration();
		await registration?.update();
	}
});

Ceci ne va pas faire en sorte que le nouveau service worker (s’il existe) se mette en place sur la page existante immédiatement — à la place, ce nouveau service worker sera installé en toile de fond, et il se mettra en place dès que le nombre d’onglets gérés par le service worker actuel tombera à zéro.

Autres solutions

L’implémentation du service worker de SvelteKit est conçue pour être simple d’usage et est probablement une bonne solution pour la plupart des gens. Cependant, en dehors de SvelteKit, beaucoup d’applications PWA utilisent la librairie Workbox. Si vous êtes habitué•e à cette librairie, vous pourriez préférer l’usage du plugin PWA de Vite.

Références

Pour plus d’informations sur les service workers de manière générale, nous recommandons la documentation de MDN.

Modifier cette page sur Github llms.txt

précédent suivant