I am trying to understand the Service Worker API, and I know the bits and parts about registering a Service Worker.
As stated in the API doc, if a service worker update is found, the service worker is registered and added to the queue. This SW takes over a page if and only if, the page is closed and opened again.That is, A window is closed and reopened again.
Now, this has a few downfalls:
The user might be seeing a previous version that might have a very serious grammatical mistake, or whatever.
The user needs to be somehow notified that the content has changed and that a referesh would do it.
I know how to tell the SW.js to skipWaiting() and take over. I also know how to send a message to the SW.js telling it that the user wants a automatic refresh.
However, what I do not know is how to know whether a new SW is actually in a waiting state.
I have used this:
navigator.serviceWorker.ready.then((a) => {
console.log("Response, ", a);
if (a.waiting !== null && a.waiting.state === "installed") {
console.log("okay");
}
});
However, it usually returns the waiting state as null.(Possibly because the SW is still installing when the request is fired.)
How can I know on the client page that a waiting service worker is available?
Here's some code that will detect and allow you to handle various states whenever there's a new or updated service worker registration.
Please note that the log message assumes that skipWaiting() is not being called during the service worker's installation; if it is being called, then instead of having to close all tabs to get the new service worker to activate, it will just activate automatically.
if ('serviceWorker' in navigator) {
window.addEventListener('load', async function() {
const registration = await navigator.serviceWorker.register('/service-worker.js');
if (registration.waiting && registration.active) {
// The page has been loaded when there's already a waiting and active SW.
// This would happen if skipWaiting() isn't being called, and there are
// still old tabs open.
console.log('Please close all tabs to get updates.');
} else {
// updatefound is also fired for the very first install. ¯\_(ツ)_/¯
registration.addEventListener('updatefound', () => {
registration.installing.addEventListener('statechange', () => {
if (event.target.state === 'installed') {
if (registration.active) {
// If there's already an active SW, and skipWaiting() is not
// called in the SW, then the user needs to close all their
// tabs before they'll get updates.
console.log('Please close all tabs to get updates.');
} else {
// Otherwise, this newly installed SW will soon become the
// active SW. Rather than explicitly wait for that to happen,
// just show the initial "content is cached" message.
console.log('Content is cached for the first time!');
}
}
});
});
}
});
}
Related
I'm still doing experiments in order to master service workers, and I'm facing a problem, probably because of my lack of expertise in JavaScript and service workers.
The problem happens when I want the new service worker to skipWaiting() using postMessage(). If I show a popup with a button and I bind a call to postMessage() there, everything works. If I call postMessage() directly, it doesn't work. It's a race condition because SOMETIMES it works, but I can't identify the race condition.
BTW, the postMessage() call WORKS, the service worker is logging what it should when getting the message:
// Listen to messages from clients.
self.addEventListener('message', event => {
switch(event.data) {
case 'skipWaiting': self.skipWaiting(); console.log('I skipped waiting... EXTRA');
break;
}
});
Here is the code. The important bit is on the if (registration.waiting) conditional. The uncommented code works, the commented one doesn't:
// Register service worker.
if ('serviceWorker' in navigator) {
// Helpers to show and hide the update toast.
let hideUpdateToast = () => {
document.getElementById('update_available').style.visibility = 'hidden';
};
let showUpdateToast = (serviceworker) => {
document.getElementById('update_available').style.visibility = 'visible';
document.getElementById('force_install').onclick = () => {
serviceworker.postMessage('skipWaiting');
hideUpdateToast();
};
document.getElementById('close').onclick = () => hideUpdateToast();
};
window.addEventListener('load', () => {
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});
navigator.serviceWorker.register('/sw.js').then(registration => {
// A new service worker has been fetched, watch for state changes.
//
// This event is fired EVERY TIME a service worker is fetched and
// succesfully parsed and goes into 'installing' state. This
// happens, too, the very first time the page is visited, the very
// first time a service worker is fetched for this page, when the
// page doesn't have a controller, but in that case there's no new
// version available and the notification must not appear.
//
// So, if the page doesn't have a controller, no notification shown.
registration.addEventListener('updatefound', () => {
// return; // FIXME
registration.installing.onstatechange = function () { // No arrow function because 'this' is needed.
if (this.state == 'installed') {
if (!navigator.serviceWorker.controller) {
console.log('First install for this service worker.');
} else {
console.log('New service worker is ready to activate.');
showUpdateToast(this);
}
}
};
});
// If a service worker is in 'waiting' state, then maybe the user
// dismissed the notification when the service worker was in the
// 'installing' state or maybe the 'updatefound' event was fired
// before it could be listened, or something like that. Anyway, in
// that case the notification has to be shown again.
//
if (registration.waiting) {
console.log('New service worker is waiting.');
// showUpdateToast(registration.waiting);
// The above works, but this DOESN'T WORK.
registration.waiting.postMessage('skipWaiting');
}
}).catch(error => {
console.log('Service worker registration failed!');
console.log(error);
});
});
}
Why does the indirect call using a button onclick event works, but calling postMessage() doesn't?
I'm absolutely at a loss and I bet the answer is simple and I'm just too blind to see it.
Thanks a lot in advance.
Looks like a bug in Chromium or WebKit, because this code works all the time in Firefox but fails in Chrome and Edge most of the time.
I've reported that bug to Chromium, let's see if it is a bug or my code is weird. I've managed to build the smallest code possible that still reproduces the issue, it's quite small and it can be found on the bug report.
The report can be found here.
Sorry for the noise, I'll keep investigating the issue but no matter how I tried, the code still fails from time to time, I can't spot the race condition on my code.
I have this very basic question
I'm striving to understand the Service Worker life cycle, or even better, what in practical terms initialize and change the states.
I got 2 questions right now:
1 - in chrome://inspect/#service-workers there are always 2 ou 3 lines, showing service workers all running with the same PID. Why? Why not only one?
2- When i inspect my service worker on refresh i got this:
#566 activated and is running [stop]
#570 waiting to activate [skipWaiting]
What does that mean? What is 566 and what is 570? I suppose they are instances of the the sw, but why there are two of them? And why 570 is still waiting? What do I have to do to make sure it will be registered-installed-activated?
3- General questions
What ends the install event in a normal life cycle?
What fires the activate event in a normal life cycle?
index.html
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('./sw.js')
.then(function(registration) {
// successful
console.log('Success: ', registration);
}).catch(function(err) {
// registration failed
console.log('Error: ', err);
});
});
}
</script>
sw.js
var cache_name = 'v1';
var cache_files = [
'./',
'./index.html',
'./style.css'
]
self.addEventListener('install', function(e){
console.log('SW install:', e);
e.waitUntil(
caches.open(cache_name)
.then(function(cache){
console.log('cache', cache);
return cache.addAll(cache_files);
})
.then(function(cache){
console.log('Cache completed');
})
)
});
self.addEventListener('activate', function(event) {
console.log('SW activate:', event);
});
self.addEventListener('fetch', function(e){
console.log('SW fetch:', e.request.url)
e.respondWith(
caches.match(e.request)
.then(function(cache_response){
if(cache_response) return cache_response;
return fetch(e.request);
})
.catch(function(err){
console.log('Cache error', err)
})
);
});
Thanks!
The ids shown by Chrome Devtools are internal. Just to point out. So they name all the Service Workers by an id. That's all.
The reason for having two SWs at the "same time" is that you had one, then you reloaded the page, navigated away and came back, or something along those lines, and you got another one. But at this point in time, when you "just got another one", it has yet to be activated and the previous SW is still controlling the page. The new SW will take control over the previous SW when you navigate back to the site from somewhere else, refreshing the page isn't enough. Basically this means closing all tabs and windows of the page and then loading it again, then the new SW takes over.
The time when the new SW hasn't taken over is called waiting state which happens between installation and activation. That can be skipped by calling self.skipWaiting() from inside the install handler of the SW.
The basic idea behind this flow is the page shouldn't be controlled by a SW that didn't control the page when the page was loaded – for this reason the first visit to a site that registers an SW will not be controlled by that SW, only the second time the SW will be activated etc.
You should REALLY read this brilliant article: The Service Worker Lifecycle
Is it possible to tell the browser to start the ServiceWorker activation and installation phases earlier?
When I look at our performance in Chrome, which you can see below, the worker is loaded within the first 1.5s, but the activation takes place not before 3.3s! Is there a way to tell the browser to do the ServiceWorker installing and activation as soon as possible?
Here is my ServiceWorker code:
self.addEventListener('install', (event) => {
// Bypass the waiting lifecycle stage,
// Just in case there's an older version of this SW registration.
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', (event) => {
// Take control of all pages under this SW's scope immediately,
// Instead of waiting for reload/navigation.
event.waitUntil(self.clients.claim());
});
ServiceWorkerContainer.ready resolves to an active ServiceWorkerRegistration, but if there are multiple service workers in play (e.g. service workers from previous page loads, multiple registrations within the same page) there are multiple service workers it could resolve to. How can I make sure that a particular service worker is handling network events when a chunk of code is executed?
That is, how can I write a function registerReady(scriptURL, options) that can be used as follows:
registerReady("foo.js").then(function (r) {
// "foo.js" is active and r.active === navigator.serviceWorker.controller
fetch("/"); // uses foo.js's "fetch" handler
});
registerReady("bar.js").then(function (r) {
// "bar.js" is active and r.active === navigator.serviceWorker.controller
fetch("/"); // use bar.js's "fetch" handler
});
First, notice there can be only one active service worker per scope and your client page can be controlled by at most one and only one service worker. So, when ready resolves, you know for sure the service worker controlling your page is active.
To know if an arbitrary sw is active you can register it and check the queue of service workers in the registration and listen for changes in their state. For instance:
function registerReady(swScript, options) {
return navigator.serviceWorker.register(swScript, options)
.then(reg => {
// If there is an active worker and nothing incoming, we are done.
var incomingSw = reg.installing || reg.waiting;
if (reg.active && !incomingSw) {
return Promise.resolve();
}
// If not, wait for the newest service worker to become activated.
return new Promise(fulfill => {
incomingSw.onstatechange = evt => {
if (evt.target.state === 'activated') {
incomingSw.onstatechange = null;
return fulfill();
}
};
});
})
}
Hope it makes sense to you.
I am using Chrome stable 46.
I have a very basic site. One button.onclick that fetch('a') and one <a> tag that opens b. Both urls a and b do not exist, I intend to catch them in the service worker and just return a new Response().
If I click on the button which fetches a, the service worker is not involved and I get a 404 error.
But if I click on the link that goes to b, the service worker onfetches and returns the response.
Question: Why is the service worker not getting any FetchEvent for the fetch('a') request?
See the files below
HTML
<html>
<body>
<p><button onclick="fetch('a');">fetch a</button></p>
<p>go to b</p>
<script src="js.js" defer></script>
</body>
</html>
js.js
navigator.serviceWorker.register('sw.js');
sw.js
self.onfetch = function(event) {
var request = event.request;
event.respondWith(
caches.match(request).then(function(response) {
return new Response('here is your onfetch response');
})
);
};
The service worker's fetch event handler isn't being called the first time you load the page because the service worker hasn't yet "claimed" the page, meaning it's not under the service worker's control. By default, the first time you visit a site that registers a service worker, the service worker's install (and potentially activate) event handlers will run, but fetch won't get triggered until the service worker takes control. That happens the next time you navigate to a page under the service worker's scope, or if you reload the current page.
You can override this default behavior and have the service worker take control during the initial navigation by calling self.clients.claim() inside your activate event handler.
I also want to point out that the fetch event handler in your sample has some issues—there's no reason to call caches.match(request) if you're always planning on returning a new Response object. More importantly, you need to do some sort of check of the event.request.url value and only return the new Response if it matches one of your "special" URLs, which in your case is a and b. The way things are implemented now, your fetch handler will return the dummy Response unconditionally, even if it's a subsequent request for your main page (index.html). That's presumably not what you want.
I put a gist together that modifies your service worker code to accomplish what I believe you're attempting. You can try out a live version thanks to RawGit. For posterity, here's the modified service worker code:
self.addEventListener('install', event => {
// Bypass the waiting lifecycle stage,
// just in case there's an older version of this SW registration.
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', event => {
// Take control of all pages under this SW's scope immediately,
// instead of waiting for reload/navigation.
event.waitUntil(self.clients.claim());
});
self.addEventListener('fetch', event => {
// In a real app, you'd use a more sophisticated URL check.
if (event.request.url.match(/a|b$/)) {
event.respondWith(new Response('here is your onfetch response'));
}
});