Lorsqu'un agent de service met à jour, il ne prend pas le contrôle de la page de manière appropriée; il entre dans un état "en attente", dans l'attente d'être activé.
Étonnamment, le technicien de service mis à jour ne prend même pas le contrôle de l'onglet après avoir actualisé la page. Google explique:
https://developers.google.com/web/fundamentals/instant-and-offline/service-worker/lifecycle
Même si vous n'avez qu'un seul onglet ouvert pour la démo, l'actualisation de la page ne suffit pas pour laisser la nouvelle version prendre le relais. Cela est dû au fonctionnement de la navigation par navigateur. Lorsque vous naviguez, la page en cours ne disparaît que lorsque les en-têtes de réponse ont été reçus. Même dans ce cas, la page en cours peut rester si la réponse comporte un en-tête
Content-Disposition
. En raison de ce chevauchement, le technicien de service actuel contrôle toujours un client lors d'une actualisation.Pour obtenir la mise à jour, fermez ou naviguez en dehors de tous les onglets à l'aide du technicien de service actuel. Ensuite, lorsque vous accédez à nouveau à la démo, vous devriez voir le cheval [contenu mis à jour].
Ce modèle est similaire à la mise à jour de Chrome. Les mises à jour de Google Chrome sont téléchargées en arrière-plan, mais ne s'appliquent pas avant le redémarrage de Chrome. En attendant, vous pouvez continuer à utiliser la version actuelle sans interruption. Cependant, cela est difficile au cours du développement, mais DevTools a les moyens de le rendre plus facile, ce que je couvrirai plus tard dans cet article.
Ce comportement est logique dans le cas de plusieurs onglets. Mon application est un ensemble qui doit être mis à jour de manière atomique. nous ne pouvons pas mélanger et faire correspondre une partie de l'ancien paquet et une partie du nouveau paquet. (Dans une application native, l'atomicité est automatique et garantie.)
Mais dans le cas d'un seul onglet, que j'appellerai le "dernier onglet ouvert" de l'application, ce comportement n'est pas ce que je veux. Je souhaite actualiser le dernier onglet ouvert pour mettre à jour mon technicien.
(Il est difficile d'imaginer que quiconque veut l'ancien agent de service continue de fonctionner lorsque le dernier onglet ouvert est actualisé. L'argument "de chevauchement de la navigation" de Google me semble être une bonne excuse pour un bug malheureux.)
Mon application n'est normalement utilisée que dans un seul onglet. En production, je veux que mes utilisateurs puissent utiliser le code le plus récent simplement en actualisant la page, mais cela ne fonctionnera pas: l'ancien agent de maintenance conservera le contrôle après les actualisations.
Je ne veux pas avoir à dire aux utilisateurs, "pour recevoir les mises à jour, assurez-vous de fermer ou de naviguer loin de mon application". Je veux leur dire, "rafraîchissez-vous".
Comment puis-je activer mon agent de service mis à jour lorsque l'utilisateur actualise la page?
EDIT: Il y a une réponse à ma connaissance, rapide, facile et erronée: skipWaiting
dans l'événement d'installation du service d'assistance. skipWaiting
fera en sorte que le nouvel opérateur de service prenne effet dès que la mise à jour sera téléchargée, alors que l'ancien onglet de page est ouvert. Cela rend la mise à jour non sécurisée non atomique; c'est comme si vous remplaciez un ensemble d'applications natives lorsque l'application est en cours d'exécution. Ce n'est pas OK pour moi. Je dois attendre que l'utilisateur actualise la page du dernier onglet ouvert.
J'ai écrit un article de blog expliquant comment gérer cela. _ { https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68 } _
J'ai aussi un échantillon de travail sur github. _ { https://github.com/dfabulich/service-worker-refresh-sample } _
Il y a quatre approches:
skipWaiting()
lors de l'installation. Ceci est dangereux, car votre onglet peut contenir un mélange de contenu ancien et nouveau.skipWaiting()
LORS DE L'INSTALLATION, ET ACTUALISEZ TOUS LES ONGLETS OUVERTS DANS controllerchange
C'est mieux, mais vos utilisateurs risquent d'être surpris lorsque l'onglet est actualisé de manière aléatoire.controllerchange
; lors de l'installation, invitez l'utilisateur à skipWaiting()
avec un bouton "Actualiser" intégré à l'application. Encore mieux, mais l'utilisateur doit utiliser le bouton "Actualiser" intégré à l'application; le bouton d'actualisation du navigateur ne fonctionne toujours pas. Ceci est documenté en détail dans les cours Udacity de Google sur les techniciens de service et le Workbox Advanced Guide ainsi que dans la branche master
de mon exemple sur Github .navigate
, comptez les onglets ouverts ("clients"). S'il n'y a qu'un seul onglet ouvert, alors skipWaiting()
immédiatement et actualisez le seul onglet en renvoyant une réponse vide avec un en-tête Refresh: 0
. Vous pouvez en voir un exemple de travail dans la branche refresh-last-tab
de mon exemple sur Github .Mettre tous ensemble...
Dans le travailleur de service:
addEventListener('message', messageEvent => {
if (messageEvent.data === 'skipWaiting') return skipWaiting();
});
addEventListener('fetch', event => {
event.respondWith((async () => {
if (event.request.mode === "navigate" &&
event.request.method === "GET" &&
registration.waiting &&
(await clients.matchAll()).length < 2
) {
registration.waiting.postMessage('skipWaiting');
return new Response("", {headers: {"Refresh": "0"}});
}
return await caches.match(event.request) ||
fetch(event.request);
})());
});
Dans la page:
function listenForWaitingServiceWorker(reg, callback) {
function awaitStateChange() {
reg.installing.addEventListener('statechange', function() {
if (this.state === 'installed') callback(reg);
});
}
if (!reg) return;
if (reg.waiting) return callback(reg);
if (reg.installing) awaitStateChange();
reg.addEventListener('updatefound', awaitStateChange);
}
// reload once when the new Service Worker starts activating
var refreshing;
navigator.serviceWorker.addEventListener('controllerchange',
function() {
if (refreshing) return;
refreshing = true;
window.location.reload();
}
);
function promptUserToRefresh(reg) {
// this is just an example
// don't use window.confirm in real life; it's terrible
if (window.confirm("New version available! OK to refresh?")) {
reg.waiting.postMessage('skipWaiting');
}
}
listenForWaitingServiceWorker(reg, promptUserToRefresh);
En plus de la réponse de Dan;
1. Ajoutez l'écouteur d'événements skipWaiting
dans le script ServiceWork.
Dans mon cas, une application basée sur l'ARC que j'utilise cra-append-sw pour ajouter cet extrait de code au technicien de service.
self.addEventListener('message', event => {
if (!event.data) {
return;
}
if (event.data === 'skipWaiting') {
self.skipWaiting();
}
});
2. Mise à jour de register-service-worker.js
Pour moi, il était naturel de recharger la page lorsque l'utilisateur navigue sur le site. Dans le script register-service-worker, j'ai ajouté le code suivant:
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
const pushState = window.history.pushState;
window.history.pushState = function () {
// make sure that the user lands on the "next" page
pushState.apply(window.history, arguments);
// makes the new service worker active
installingWorker.postMessage('skipWaiting');
};
} else {
Nous utilisons essentiellement la fonction native pushState
pour nous assurer que l’utilisateur navigue vers une nouvelle page. Toutefois, si vous utilisez également des filtres dans votre URL pour une page de produits, par exemple, vous pouvez empêcher le rechargement de la page et attendre une meilleure opportunité.
Ensuite, j'utilise également l'extrait de code de Dan pour recharger tous les onglets lorsque le contrôleur de travailleur de maintenance change. Cela rechargera également l'onglet actif.
.catch(error => {
console.error('Error during service worker registration:', error);
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) {
return;
}
refreshing = true;
window.location.reload();
});