are network requests due to code after a service worker is registered guaranteed to be served by the service worker fetch() handler? - service-worker

Registering a service worker is done in index.html with (eg):
<script>
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
</script>
If that code is followed by something that requests a resource, eg:
<script src="a.js"></script>
is that request guaranteed to trigger the 'fetch' event handler in the service worker (and so, potentially, be served from a cache)?
Or, should any code that causes a network access in index.html be added dynamically in the then() callback of the register() function (and, is THAT then guaranteed to be served by the service worker's 'fetch' event handler)?

I would recommend reading through "The Service Worker Lifecycle" for more general information.
The answer to most of your questions is "no," since what you're talking about is the initial registration of the service worker. Registering a service worker kicks off an installation and activation process that's independent from the promise returned by register(). The only thing you could infer from that promise is whether starting the process succeeded or not.
What you're asking about—whether a fetch handler will be invoked or not—relies on a service worker being in control of the current page.
In terms of JavaScript, if you want to answer the question "is this page controlled by (any) service worker?", you can do that by checking whether or not navigator.serviceWorker.controller is undefined.
If you want to write code that will only execute once there's a service worker in control of the current page (with the caveat that it might never execute, if something prevented the service worker from properly activating), you could do that by creating a promise that will resolve immediately if there's already a controller, and will otherwise resolve once the controllerchange event fires:
const p = new Promise(r => {
if (navigator.serviceWorker.controller) return r();
navigator.serviceWorker.addEventListener('controllerchange', e => r());
});
// Later, if you want code to execute only if the page is controlled:
p.then(() => {
// There's a SW in control at this point.
});
Inside your service worker, you can add the following to your activate handler to ensure that as soon as a newly installed service worker activates, it takes control of any open pages (including the page that registered it for the first time):
self.addEventListener('activate', () => self.clients.claim());
If you don't include self.clients.claim() inside your service worker's activate handler, then the page that starts out uncontrolled will never start being controlled, even though it's registered a service worker that has activated.

Related

Service-Worker stays in waiting state in Chrome

I'm working on a SPA with Vue. I'd like to update to a new service-worker when the user navigates to a specific page. A save moment to refresh, because the view of the user already changes (a pattern discussed in this video: https://youtu.be/cElAoxhQz6w)
I have an issue that sometimes (infrequently) the service-worker won't activate while calling skipWaiting. The call is made correctly, and even in Chrome I get a response that the current service-worker stops (see animated GIF), however it the same service-worker starts running again, instead of the waiting one.
After a while (1-2 minutes) the service-worker is suddenly activated. Not a situation you want, because it happens just out of the blue when the user might be in the middle of an activity.
Also when I am in this situation I can't activate the service-worker by calling skipWaiting (by doing multiple navigations) again. It's received by the service-worker but nothing happens. It stays in "waiting to activate". When I press skipWaiting in Chrome itself, it works.
I have no clue what goes wrong. Is this an issue with Chrome, workbox or something else?
Most close comes this topic: self.skipWaiting() not working in Service Worker
I use Vue.js, but I don't depend on the pwa plugin for the service-worker. I use the workbox webpack plugin.
I've edited the example code below, the minimal code probably didn't show the problem well
In main.js:
let sw = await navigator.serviceWorker.register("/service-worker.js", {
updateViaCache: "none",
});
let firstSw = false;
navigator.serviceWorker.addEventListener("controllerchange", () => {
// no need to refresh when the first sw controls the page, we solve this with clientsClaim
// this makes sure when multiple-tabs are open all refresh
if (!firstSw) {
window.location.reload();
}
});
sw.onupdatefound = () => {
const installingWorker = sw.installing;
installingWorker.onstatechange = async () => {
console.log("installing worker state-change: " + installingWorker.state);
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
firstSw = false;
// set the waiting service-worker in the store
// so we can update it and refresh the page on navigation
await store.dispatch("setWaitingSW", sw.waiting);
} else {
console.log("First sw available");
firstSw = true;
}
}
};
};
In router.js:
// after navigation to specific routes we check for a waiting service-worker.
router.afterEach(async (to) => {
if (to.name == "specificpage") {
let waitingSw = store.getters["getWaitingSW"];
if (waitingSw) {
waitingSw.postMessage("SKIP_WAITING");
// clean the store, because we might have changed our data model
await store.dispatch("cleanLocalForage");
}
}
});
In service-worker.js:
self.addEventListener("message", event => {
if (event.data === "SKIP_WAITING") {
console.log("sw received skip waiting");
self.skipWaiting();
}
});
skipWaiting() isn't instant. If there are active fetches going through the current service worker, it won't break those. If you're seeing skipWaiting() taking a long time, I'd guess you have some long-running HTTP connections holding the old service worker in place.
I'm not sure that
let sw = await navigator.serviceWorker.register("/service-worker.js", {updateViaCache: "none"});
if (sw.waiting) {
sw.waiting.postMessage("SKIP_WAITING");
}
is the code that you want in this case. Your if (sw.waiting) check is only evaluated once, and the newly registered service worker might still be in the installing state when it's evaluated. If that's the case, then sw.waiting will be false-y at the time of initial evaluation, though it may be true-thy after a small period of time.
Instead, I'd recommend following a pattern like what's demonstrated in this recipe, where you explicitly listen for a service worker to enter waiting on the registration. That example uses the workbox-window library to paper over some of the details.
If you don't want to use workbox-window, you should follow this guidance check to see if sw.installing is set after registration; if it is, listen to the statechange event on sw.installing to detect when it's 'installed'. Once that happens, sw.waiting should be set to the newly installed service worker, and at that point, you could postMessage() to it.
Ok i had a similar issue and it took me two days to find the cause.
There is a scenario where you can cause a race condition between the new service worker and the old if you request a precached asset at the exact same time you call skip waiting.
For me i was prompting the user to update to a new version and upon their confirmation i was showing a loading spinner which was a Vue SFC dynamic import which kicked off a network request to the old service worker to fetch the precached js file which basically caused both to hang and get very confused.
You can check if your having a similar issue by looking at the service worker specific network requests (Network requests button in the image below) that are happening and make sure they aren't happening the instant you're trying to skip waiting on your newer service worker.

Fetch of the service worker doesn't seem to get triggered

When a browser requests an image from the server, the call is getting picked up by an API controller in the back end. There, a authorization check must be done before returning the image in order to check if the request is allowed or not.
So I need to add the authorization header and when searching for the best solution, I found this article: https://www.twelve21.io/how-to-access-images-securely-with-oauth-2-0/ and I was mostly intereseted in the solution number 4 which uses a Service Worker.
I made my own implementation, I registered a serviceWorker:
if ('serviceWorker' in navigator) {
console.log("serviceWorker active");
window.addEventListener('load', onLoad);
}
else {
console.log("serviceWorker not active");
}
function onLoad() {
console.log("onLoad is called");
var scope = {
scope: '/api/imagesgateway/'
};
navigator.serviceWorker.register('/Scripts/ServiceWorker/imageInterceptor.js', scope)
.then(registration => console.log("ServiceWorker registration successful with scope: ", registration.scope))
.catch(error => console.error("ServiceWorker registration failed: ", error));
}
and this is in my imageInterceptor:
self.addEventListener('fetch', event => {
console.log("fetch event triggered");
event.respondWith(
fetch(event.request, {
mode: 'cors',
credentials: 'include',
header: {
'Authorization': 'Bearer ...'
}
})
)
});
When I run my application, I see in my console that the registration seems to be successfully executed as I see the console.logs printed (ServiceWorker active, onLoad is called and successful registration with correct scope: https://localhost:44332/api/imagesgateway/
But when I load an image (https://localhost:44332/api/imagesgateway/...) via the gateway, I still get a 400 and when put a breakpoint on the backend I see that the authentication header is still null. Also, I don't see "fetch event triggered" message in my console. In another article it is stated that I can see the registered service workers via this setting: chrome://inspect/#service-workers but I don't see my worker there either.
My question is: Why isn't the authorization header added? Is it because, although the registration seems to go successfully, this isn't actually the case and therefore I don't see the worker in inspect#service-workers either?
You're not seeing fetch event triggered in the browser console because your Service Worker script isn't allowed to intercept the image requests. This is because your Service Worker script is located in a directory outside the scope of the requests you're interested in.
In order to intercept requests that handle resources at
/api/imagesgateway/
the SW script needs to be located in either
/, /api/, or /api/imagesgateway/. It cannot be located in /some/other/directory/service-worker.js.
This is the reason that your Service Worker registers successfully! There is no probelm in registering the SW. The problem lies in what it can do.
More info: Understanding Service Worker scope

Track event to Google TagManager inside a ServiceWorker

I'm working on a service-worker (really a firebase cloud-messaging serviceworker like this) and I would like to track each time a user receives a push on Google TagManager.
Any help on how to include the script and send tracks within a SW?
Thanks.
The Service worker runs outside the main thread and does not have
access to the Window object, meaning that it cannot access the data
layer or the ga command queue to create trackers. In short, the
actions of a service worker cannot be tracked using the normal
on-page, JavaScript-based tracking snippets. What we can do, however,
is configure our service worker to send HTTP hits directly to GA.
Here is a sample code
fetch('https://www.google-analytics.com/collect', {
method: 'post',
body: JSON.stringify({
v: 1, // Version Number
t: eventName, // Hit Type
ec: eventCategory, // Event Category
ea: eventAction, // Event Action
el: 'serviceworker' // Event Label
})
})
If you want to get into more details, I would recommend reading this article. https://builtvisible.com/google-analytics-for-pwas-tracking-offline-behaviour-and-more/

service worker install event is called before register event is completed

I have attached install event to service worker as below. But Install event fired before register event is completed. See code snippets and console logs below.
My concern is how install event is fired before register event is completed?
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js',{scope : '/'}).then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
}
var cacheName = 'V-1';
var filesToCache = [
'/', '/index.html',
'/css/all.css', '/css/material.min.css',
'/js/all.js', '/js/material.min.js',
'/images/service-worker-1.png','/images/service-worker-2.png','/images/service-worker-3.png',
];
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Installing');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache
.addAll(filesToCache) //this is atomic in nature i.e. if any of the file fails the entire cache step fails.
.then(() => {console.log('[ServiceWorker] App shell Caching Successful');})
.catch(() => {console.log('[ServiceWorker] App shell Caching Failed');})
})
);
});
navigator.serviceWorker.register() is not an event. It's a function that returns a promise, and then promise will resolve with a ServiceWorkerRegistration object that corresponds to the registration.
The actual service worker logic is executed in a different thread/process, and the lifecycle events that the service worker handles, like the install event, happen independently of the web page that registered the service worker. What you're seeing in your console.log() output is expected.
If you want to keep track of the state of the service worker from your web page, you can add event listeners to the ServiceWorkerRegistration object. There's an example of this at https://googlechrome.github.io/samples/service-worker/registration-events/index.html
If you want to write code that will cause your web page to wait until there's an active service worker before it takes some action, you could make use of the navigator.serviceWorker.ready promise.

How do I use ServiceWorker without a separate JS file?

We create service workers by
navigator.serviceWorker.register('sw.js', { scope: '/' });
We can create new Workers without an external file like this,
var worker = function() { console.log('worker called'); };
var blob = new Blob( [ '(' , worker.toString() , ')()' ], {
type: 'application/javascript'
});
var bloburl = URL.createObjectURL( blob );
var w = new Worker(bloburl);
With the approach of using blob to create ServiceWorkers, we will get a Security Error as the bloburl would be blob:chrome-extension..., and the origin won't be supported by Service Workers.
Is it possible to create a service worker without external file and use the scope as / ?
I would strongly recommend not trying to find a way around the requirement that the service worker implementation code live in a standalone file. There's a very important of the service worker lifecycle, updates, that relies on your browser being able to fetch your registered service worker JavaScript resource periodically and do a byte-for-byte comparison to see if anything has changed.
If something has changed in your service worker code, then the new code will be considered the installing service worker, and the old service worker code will eventually be considered the redundant service worker as soon as all pages that have the old code registered and unloaded/closed.
While a bit difficult to wrap your head around at first, understanding and making use of the different service worker lifecycle states/events are important if you're concerned about cache management. If it weren't for this update logic, once you registered a service worker for a given scope once, it would never give up control, and you'd be stuck if you had a bug in your code/needed to add new functionality.
One hacky way is to use the the same javascript file understand the context and act as a ServiceWorker as well as the one calling it.
HTML
<script src="main.js"></script>
main.js
if(!this.document) {
self.addEventListener('install', function(e) {
console.log('service worker installation');
});
} else {
navigator.serviceWorker.register('main.js')
}
To prevent maintaining this as a big file main.js, we could use,
if(!this.document) {
//service worker js
importScripts('sw.js');
else {
//loadscript document.js by injecting a script tag
}
But it might come back to using a separate sw.js file for service worker to be a better solution. This would be helpful if one'd want a single entry point to the scripts.

Resources