Le cycle de vie du service worker est sa partie la plus complexe. Si vous ne savez pas ce qu'il essaie de faire et quels sont ses avantages, vous pouvez avoir l'impression qu'il vous combat. Mais une fois que vous avez compris comment cela fonctionne, vous pouvez proposer aux utilisateurs des mises à jour fluides et discrètes, en combinant le meilleur des modèles Web et natifs.
Il s'agit d'une analyse approfondie, mais les puces au début de chaque section couvrent l'essentiel de ce que vous devez savoir.
L'intention
Le cycle de vie vise à :
- Rendre possible le mode hors connexion.
- Permet à un nouveau service worker de se préparer sans perturber le service worker actuel.
- Assurez-vous qu'une page concernée est contrôlée par le même service worker (ou aucun service worker) tout au long de l'opération.
- Assurez-vous qu'une seule version de votre site est exécutée à la fois.
Ce dernier point est très important. Sans service workers, les utilisateurs peuvent charger un onglet sur votre site, puis en ouvrir un autre plus tard. Cela peut entraîner l'exécution simultanée de deux versions de votre site. Cela ne pose parfois pas de problème, mais si vous gérez l'espace de stockage, vous pouvez facilement vous retrouver avec deux onglets ayant des opinions très différentes sur la façon dont leur espace de stockage partagé doit être géré. Cela peut entraîner des erreurs ou, pire, une perte de données.
Le premier service worker
En bref :
- L'événement
installest le premier événement qu'un service worker reçoit, et il ne se produit qu'une seule fois. - Une promesse transmise à
installEvent.waitUntil()indique la durée et la réussite ou l'échec de votre installation. - Un service worker ne recevra pas d'événements tels que
fetchetpushtant qu'il n'aura pas terminé l'installation et qu'il ne sera pas "actif". - Par défaut, les récupérations d'une page ne passent pas par un service worker, sauf si la requête de la page elle-même est passée par un service worker. Vous devrez donc actualiser la page pour voir les effets du service worker.
clients.claim()peut ignorer cette valeur par défaut et prendre le contrôle des pages non contrôlées.
Prenons le code HTML suivant :
<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered!', reg))
.catch(err => console.log('Boo!', err));
setTimeout(() => {
const img = new Image();
img.src = '/dog.svg';
document.body.appendChild(img);
}, 3000);
</script>
Il enregistre un service worker et ajoute l'image d'un chien au bout de trois secondes.
Voici son service worker, sw.js :
self.addEventListener('install', event => {
console.log('V1 installing…');
// cache a cat SVG
event.waitUntil(
caches.open('static-v1').then(cache => cache.add('/cat.svg'))
);
});
self.addEventListener('activate', event => {
console.log('V1 now ready to handle fetches!');
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// serve the cat SVG from the cache if the request is
// same-origin and the path is '/dog.svg'
if (url.origin == location.origin && url.pathname == '/dog.svg') {
event.respondWith(caches.match('/cat.svg'));
}
});
Il met en cache une image de chat et la diffuse chaque fois qu'une requête est envoyée pour /dog.svg. Toutefois, si vous exécutez l'exemple ci-dessus, vous verrez un chien la première fois que vous chargerez la page. Actualisez la page pour voir le chat.
Champ d'application et contrôle
Le niveau d'accès par défaut d'un enregistrement de service worker est ./ par rapport à l'URL du script. Cela signifie que si vous enregistrez un service worker sur //example.com/foo/bar.js, son champ d'application par défaut est //example.com/foo/.
Nous appelons les pages, les workers et les workers partagés des clients. Votre service worker ne peut contrôler que les clients qui se trouvent dans le champ d'application. Une fois qu'un client est "contrôlé", ses récupérations passent par le service worker concerné. Vous pouvez détecter si un client est contrôlé via navigator.serviceWorker.controller, qui sera nul ou une instance de service worker.
Télécharger, analyser et exécuter
Votre tout premier service worker est téléchargé lorsque vous appelez .register(). Si le téléchargement ou l'analyse de votre script échoue, ou si une erreur est générée lors de son exécution initiale, la promesse d'enregistrement est rejetée et le service worker est supprimé.
Les outils de développement de Chrome affichent l'erreur dans la console et dans la section "Service worker" de l'onglet "Application" :
Installer
Le premier événement qu'un service worker reçoit est install. Il est déclenché dès que le service worker s'exécute et n'est appelé qu'une seule fois par service worker. Si vous modifiez votre script de service worker, le navigateur le considère comme un service worker différent, et il recevra son propre événement install. Je reviendrai sur les mises à jour en détail plus tard.
L'événement install vous permet de mettre en cache tout ce dont vous avez besoin avant de pouvoir contrôler les clients. La promesse que vous transmettez à event.waitUntil() permet au navigateur de savoir quand l'installation est terminée et si elle a réussi.
Si votre promesse est rejetée, cela signifie que l'installation a échoué et le navigateur supprime le service worker. Il ne contrôlera jamais les clients. Cela signifie que nous pouvons compter sur la présence de cat.svg dans le cache de nos événements fetch. Il s'agit d'une dépendance.
Activer
Une fois que votre service worker est prêt à contrôler les clients et à gérer les événements fonctionnels tels que push et sync, vous recevez un événement activate. Toutefois, cela ne signifie pas que la page qui a appelé .register() sera contrôlée.
La première fois que vous chargez la démo, même si dog.svg est demandé longtemps après l'activation du service worker, il ne gère pas la requête et vous voyez toujours l'image du chien. La valeur par défaut est cohérence. Si votre page se charge sans service worker, ses sous-ressources ne se chargeront pas non plus. Si vous chargez la démo une deuxième fois (en d'autres termes, si vous actualisez la page), elle sera contrôlée. La page et l'image passeront par des événements fetch, et vous verrez un chat à la place.
clients.claim
Vous pouvez prendre le contrôle des clients non contrôlés en appelant clients.claim() dans votre service worker une fois qu'il est activé.
Voici une variante de la démonstration ci-dessus qui appelle clients.claim() dans son événement activate. Vous devriez voir un chat la première fois. Je dis "devrait", car cela dépend du timing. Vous ne verrez un chat que si le service worker s'active et que clients.claim() prend effet avant que l'image ne tente de se charger.
Si vous utilisez votre service worker pour charger des pages différemment de la façon dont elles se chargeraient via le réseau, clients.claim() peut poser problème, car votre service worker finit par contrôler certains clients qui se sont chargés sans lui.
Mettre à jour le service worker
En bref :
- Une mise à jour est déclenchée si l'un des événements suivants se produit :
- Une navigation vers une page incluse dans le champ d'application.
- Un événement fonctionnel tel que
pushetsync, sauf si une recherche de mise à jour a été effectuée au cours des 24 dernières heures. - Appeler
.register()uniquement si l'URL du service worker a changé. Toutefois, vous devez éviter de modifier l'URL du nœud de calcul.
- La plupart des navigateurs, y compris Chrome 68 et versions ultérieures, ignorent par défaut les en-têtes de mise en cache lorsqu'ils recherchent des mises à jour du script de service worker enregistré. Ils respectent toujours les en-têtes de mise en cache lors de la récupération des ressources chargées dans un service worker via
importScripts(). Vous pouvez remplacer ce comportement par défaut en définissant l'optionupdateViaCachelors de l'enregistrement de votre service worker. - Votre service worker est considéré comme mis à jour s'il est différent de celui que le navigateur possède déjà. (Nous étendons également cette fonctionnalité aux scripts/modules importés.)
- Le service worker mis à jour est lancé en même temps que le service worker existant et reçoit son propre événement
install. - Si votre nouveau worker renvoie un code d'état autre que "OK" (par exemple, 404), ne parvient pas à analyser, génère une erreur lors de l'exécution ou est refusé lors de l'installation, il est supprimé, mais le worker actuel reste actif.
- Une fois l'installation terminée, le nœud de calcul mis à jour
waitjusqu'à ce que le nœud de calcul existant ne contrôle plus aucun client. (Notez que les clients se chevauchent lors d'une actualisation.) self.skipWaiting()empêche l'attente, ce qui signifie que le service worker s'active dès qu'il a terminé l'installation.
Imaginons que nous ayons modifié notre script de service worker pour qu'il réponde avec l'image d'un cheval plutôt que d'un chat :
const expectedCaches = ['static-v2'];
self.addEventListener('install', event => {
console.log('V2 installing…');
// cache a horse SVG into a new cache, static-v2
event.waitUntil(
caches.open('static-v2').then(cache => cache.add('/horse.svg'))
);
});
self.addEventListener('activate', event => {
// delete any caches that aren't in expectedCaches
// which will get rid of static-v1
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.map(key => {
if (!expectedCaches.includes(key)) {
return caches.delete(key);
}
})
)).then(() => {
console.log('V2 now ready to handle fetches!');
})
);
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// serve the horse SVG from the cache if the request is
// same-origin and the path is '/dog.svg'
if (url.origin == location.origin && url.pathname == '/dog.svg') {
event.respondWith(caches.match('/horse.svg'));
}
});
Regardez une démonstration de ce qui précède. L'image d'un chat devrait toujours s'afficher. Voici pourquoi…
Installer
Notez que j'ai remplacé le nom du cache static-v1 par static-v2. Cela signifie que je peux configurer le nouveau cache sans écraser les éléments du cache actuel, que l'ancien service worker utilise toujours.
Ce modèle crée des caches spécifiques à une version, semblables aux éléments qu'une application native regrouperait avec son exécutable. Vous pouvez également avoir des caches qui ne sont pas spécifiques à une version, comme avatars.
En attente
Une fois l'installation terminée, le service worker mis à jour retarde l'activation jusqu'à ce que le service worker existant ne contrôle plus les clients. Cet état est appelé "en attente". C'est ainsi que le navigateur s'assure qu'une seule version de votre service worker est exécutée à la fois.
Si vous avez exécuté la démo mise à jour, vous devriez toujours voir une image de chat, car le worker V2 n'a pas encore été activé. Vous pouvez voir le nouveau service worker en attente dans l'onglet "Application" des outils de développement :
Même si un seul onglet est ouvert sur la démo, actualiser la page ne suffit pas pour que la nouvelle version prenne le relais. Cela est dû au fonctionnement des navigations dans le navigateur. Lorsque vous naviguez, la page actuelle ne disparaît pas tant que les en-têtes de réponse n'ont pas été reçus. Même dans ce cas, la page actuelle peut rester si la réponse comporte un en-tête Content-Disposition. En raison de ce chevauchement, le service worker actuel contrôle toujours un client lors d'une actualisation.
Pour obtenir la mise à jour, fermez tous les onglets utilisant le service worker actuel ou quittez-les. Ensuite, lorsque vous accéderez à nouveau à la démonstration, vous devriez voir le cheval.
Ce modèle est semblable à celui de la mise à jour de Chrome. Les mises à jour de Chrome sont téléchargées en arrière-plan, mais ne sont appliquées qu'au redémarrage de Chrome. En attendant, vous pouvez continuer à utiliser la version actuelle sans interruption. Toutefois, cela peut être fastidieux lors du développement, mais les outils de développement proposent des solutions pour simplifier les choses, que j'aborderai plus loin dans cet article.
Activer
Il se déclenche une fois que l'ancien service worker a disparu et que le nouveau peut contrôler les clients. C'est le moment idéal pour effectuer des opérations que vous ne pouviez pas faire pendant que l'ancien nœud de calcul était encore utilisé, comme migrer des bases de données et vider les caches.
Dans la démonstration ci-dessus, je gère une liste de caches que je m'attends à trouver, et dans l'événement activate, je me débarrasse de tous les autres, ce qui supprime l'ancien cache static-v1.
Si vous transmettez une promesse à event.waitUntil(), elle mettra en mémoire tampon les événements fonctionnels (fetch, push, sync, etc.) jusqu'à ce que la promesse soit résolue. Ainsi, lorsque votre événement fetch se déclenche, l'activation est entièrement terminée.
Ignorer la phase d'attente
La phase d'attente signifie que vous n'exécutez qu'une seule version de votre site à la fois. Toutefois, si vous n'avez pas besoin de cette fonctionnalité, vous pouvez activer votre nouveau service worker plus tôt en appelant self.skipWaiting().
Cela permet à votre service worker d'expulser le worker actif actuel et de s'activer dès qu'il entre dans la phase d'attente (ou immédiatement s'il est déjà dans la phase d'attente). Cela n'entraîne pas l'annulation de l'installation par votre nœud de calcul, mais seulement une attente.
Le moment où vous appelez skipWaiting() n'a pas vraiment d'importance, tant que c'est pendant ou avant l'attente. Il est assez courant de l'appeler dans l'événement install :
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
// caching etc
);
});
Toutefois, vous pouvez l'appeler à la suite d'un postMessage() au service worker. Par exemple, vous souhaitez skipWaiting() après une interaction de l'utilisateur.
Voici une démonstration qui utilise skipWaiting(). L'image d'une vache devrait s'afficher sans que vous ayez à quitter la page. Comme pour clients.claim(), il s'agit d'une course. Vous ne verrez donc la vache que si le nouveau service worker récupère, installe et active l'image avant que la page ne tente de la charger.
Mises à jour manuelles
Comme indiqué précédemment, le navigateur recherche automatiquement les mises à jour après les navigations et les événements fonctionnels, mais vous pouvez également les déclencher manuellement :
navigator.serviceWorker.register('/sw.js').then(reg => {
// sometime later…
reg.update();
});
Si vous pensez que l'utilisateur utilisera votre site pendant une longue période sans le recharger, vous pouvez appeler update() à intervalles réguliers (par exemple, toutes les heures).
Évitez de modifier l'URL de votre script de service worker
Si vous avez lu mon article sur les bonnes pratiques de mise en cache, vous pouvez envisager d'attribuer une URL unique à chaque version de votre service worker. À ne pas faire Il s'agit généralement d'une mauvaise pratique pour les service workers. Mettez simplement à jour le script à son emplacement actuel.
Cela peut vous poser un problème comme celui-ci :
index.htmlenregistresw-v1.jsen tant que service worker.sw-v1.jsmet en cache et diffuseindex.htmlpour qu'il fonctionne hors connexion.- Vous mettez à jour
index.htmlpour qu'il enregistre votre nouveausw-v2.js.
Si vous procédez ainsi, l'utilisateur n'obtient jamais sw-v2.js, car sw-v1.js diffuse l'ancienne version de index.html à partir de son cache. Vous vous êtes mis dans une situation où vous devez mettre à jour votre service worker pour pouvoir le mettre à jour. Beurk.
Toutefois, pour la démonstration ci-dessus, j'ai modifié l'URL du service worker. Cela vous permet de passer d'une version à l'autre pour les besoins de la démonstration. Je ne ferais pas cela en production.
Simplifier le développement
Le cycle de vie du service worker est conçu en pensant à l'utilisateur, mais pendant le développement, il peut être un peu difficile à gérer. Heureusement, il existe quelques outils pour vous aider :
Mettre à jour lors de l'actualisation
C'est mon préféré.
Cela permet de modifier le cycle de vie pour qu'il soit plus adapté aux développeurs. Chaque navigation :
- Récupérez à nouveau le service worker.
- Installez-le en tant que nouvelle version, même s'il est identique au niveau des octets. Cela signifie que votre événement
installs'exécute et que vos caches sont mis à jour. - Ignorer la phase d'attente pour que le nouveau service worker s'active.
- Parcourez la page.
Cela signifie que vous recevrez vos mises à jour à chaque navigation (y compris l'actualisation) sans avoir à actualiser deux fois ou à fermer l'onglet.
Ignorer l'attente
Si un worker est en attente, vous pouvez cliquer sur "Ignorer l'attente" dans les outils de développement pour le faire passer immédiatement à l'état "actif".
Maj+Actualiser
Si vous forcez l'actualisation de la page (Maj+Actualiser), le service worker est complètement ignoré. Il sera incontrôlé. Cette fonctionnalité est incluse dans la spécification. Elle fonctionne donc dans les autres navigateurs compatibles avec les service workers.
Gérer les mises à jour
Le service worker a été conçu dans le cadre du Web extensible. L'idée est que nous, en tant que développeurs de navigateurs, reconnaissons que nous ne sommes pas meilleurs en développement Web que les développeurs Web. Par conséquent, nous ne devrions pas fournir d'API de haut niveau étroites qui résolvent un problème particulier à l'aide de modèles que nous aimons, mais plutôt vous donner accès aux entrailles du navigateur et vous laisser faire comme vous le souhaitez, de la manière qui convient le mieux à vos utilisateurs.
Pour permettre l'utilisation d'un maximum de modèles, l'ensemble du cycle de mise à jour est observable :
navigator.serviceWorker.register('/sw.js').then(reg => {
reg.installing; // the installing worker, or undefined
reg.waiting; // the waiting worker, or undefined
reg.active; // the active worker, or undefined
reg.addEventListener('updatefound', () => {
// A wild service worker has appeared in reg.installing!
const newWorker = reg.installing;
newWorker.state;
// "installing" - the install event has fired, but not yet complete
// "installed" - install complete
// "activating" - the activate event has fired, but not yet complete
// "activated" - fully active
// "redundant" - discarded. Either failed install, or it's been
// replaced by a newer version
newWorker.addEventListener('statechange', () => {
// newWorker.state has changed
});
});
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
// This fires when the service worker controlling this page
// changes, eg a new worker has skipped waiting and become
// the new active worker.
});
Le cycle de vie continue
Comme vous pouvez le constater, il est utile de comprendre le cycle de vie des service workers. Grâce à cette compréhension, les comportements des service workers devraient sembler plus logiques et moins mystérieux. Ces connaissances vous donneront plus de confiance lorsque vous déploierez et mettrez à jour des service workers.