I'm using this well-known pattern for showing a notification when a service worker update is ready to install (this code goes into the web page, it's NOT the service worker code, of course):
// Register service worker.
let newWorker;
if ('serviceWorker' in navigator) {
function showUpdateNotification () {
document.getElementById('updatenotification').style['visibility'] = 'visible';
};
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(registration => {
console.log('Service worker registered at scope "' + registration.scope + '".');
// The commented code below is needed to show the notification after a page reload.
//
// if (registration.waiting) {
// console.log('Service working in skipwaiting state.');
// showUpdateNotification();
// }
registration.onupdatefound = () => {
console.log('Service worker update found.');
console.log('Installing service worker is', registration.installing);
newWorker = registration.installing;
newWorker.onstatechange = function () {
console.log('Service worker state changed to', newWorker.state);
if (newWorker.state == 'installed' && navigator.serviceWorker.controller) {
console.log('New service worker is ready to install on refresh.');
showUpdateNotification();
}
};
};
console.log('Updating service worker.');
registration.update();
}).catch(error => console.log('Service worker not registered (' + error +').'))
})
}
Of course, that code works, in the sense that it shows a notification on the web page if a new version of the service worker is ready to be installed.
The problem is that if the page is reloaded at that point, the notification is no longer shown, because if the new service worker is installed and waiting to activate, the updatefound event is no longer fired.
So, the notification only appears ONCE, when the new service worker is installed and waiting to be activated and start controlling the page, but once the page reloads, the notification is gone.
I've solved that by using the commented code:
// The commented code below is needed to show the notification after a page reload.
//
// if (registration.waiting) {
// console.log('Service working in skipwaiting state.');
// showUpdateNotification();
// }
This code, upon registration, checks if there's some service worker in waiting state and shows, again, the notification.
My question is: is this correct? Can I use that "trick" safely or am I calling trouble?
I'm new to service workers so I'm not sure if I can do this kind of things.
Thanks A LOT in advance.
Well, sorry for the self-reply, but more or less I got what I needed, while at the same time handling all the cases (at least, all the cases I need for my project).
I think the code below more or less covers the handling of the entire life-cycle of a service worker, and can be used as a non-very sophisticated boilerplate code for that.
I designed this code using information from a myriad of sources, including StackOverflow, blogs, code from other PWAs, etc. Unfortunately I didn't wrote down each and every source of information, so I'm very sorry for that and I wanted to make clear that I wrote the code below but I didn't invent it, I used information and wisdom from others.
Thanks a lot!
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('New service worker in charge.');
if (refreshing) return;
refreshing = true;
window.location.reload();
});
navigator.serviceWorker.register('/sw.js').then(registration => {
console.log('Service worker registered.');
// No controller for this page, nothing to do for now.
if (!navigator.serviceWorker.controller) {
console.log('No service worker controlling this page.');
}
// 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', function () {
console.log('New service worker in installing state.');
registration.installing.onstatechange = function () {
console.log('Service worker state changed to', registration.state);
if (registration.state == 'installed') {
if (!navigator.serviceWorker.controller) {
console.log('First install for this service worker.');
} else {
console.log('New service worker is ready to install on refresh.');
}
}
};
});
// 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('Service working in skipwaiting state.');
}
// Well, really this should go into a setInterval() call, but I'm
// including it here to be exhaustive.
console.log('Updating service worker.');
registration.update();
}).catch(error => console.log('Service worker not registered (' + error +').'))
})
} else {
console.log('Service workers not supported.');
}
Related
I have a set of integration tests which run in Karma. Unfortunately, they call out to an external, production API endpoint. I do not want integration tests to call out and am exploring my options.
I am wondering if service workers are a viable solution. My assumption is that they do not work because https://github.com/w3c/ServiceWorker/issues/1188 makes it clear that cross-origin fetch is not supported and localhost is not the same origin as a production API endpoint.
For clarity, here is some code I am running:
try {
const { scope, installing, waiting, active } = await navigator.serviceWorker.register('./base/htdocs/test/imageMock.sw.js');
console.log('ServiceWorker registration successful with scope: ', scope, installing, waiting, active);
(installing || waiting || active).addEventListener('statechange', (e) => {
console.log('state', e.target.state);
});
} catch (error) {
console.error('ServiceWorker registration failed: ', error);
}
and the service worker
// imageMock.sw.js
if (typeof self.skipWaiting === 'function') {
console.log('self.skipWaiting() is supported.');
self.addEventListener('install', (e) => {
// See https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-global-scope-skipwaiting
e.waitUntil(self.skipWaiting());
});
} else {
console.log('self.skipWaiting() is not supported.');
}
if (self.clients && (typeof self.clients.claim === 'function')) {
console.log('self.clients.claim() is supported.');
self.addEventListener('activate', (e) => {
// See https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#clients-claim-method
e.waitUntil(self.clients.claim());
});
} else {
console.log('self.clients.claim() is not supported.');
}
self.addEventListener('fetch', (event) => {
console.log('fetching resource', event);
if (/\.jpg$/.test(event.request.url)) {
const response = new Response('<p>This is a response that comes from your service worker!</p>', {
headers: { 'Content-Type': 'text/html' },
});
event.respondWith(response);
}
});
and when this code is ran I see in the console
ServiceWorker registration successful with scope: http://localhost:9876/base/htdocs/test/ null null ServiceWorker
and then requests to https://<productionServer>.com/image.php are not intercepted by the fetch handler.
Is it correct that there is no way to intercept in this scenario?
You can use a service worker to intercept requests made by a browser as part of a test suite. As long a service worker is in control of a web page, it can intercept cross-origin requests and generate any response you'd like.
(The issue you link to about "foreign fetch" is something different; think of it as the remote production server deploying its own service worker. This was abandoned.)
"Stop mocking fetch" is a comprehensive article covering how to use the msw service worker library within the context of a test suite.
I can't say off the top of my head exactly why your current setup isn't working, but from past experience, I can say that the most important thing to remember when doing this is that you need to delay making any requests from a client test page until the page itself is being controlled by an active service worker. Otherwise, there's a race condition in which you might end up firing off a request that needs to trigger a fetch handler, but won't if the service worker isn't yet in control.
You can wait for this to happen with logic like:
const controlledPromise = new Promise((resolve) => {
// Resolve right away if this page is already controlled.
if (navigator.serviceWorker.controller) {
resolve();
} else {
navigator.serviceWorker.addEventListener('controllerchange', () => {
resolve();
});
}
});
await controlledPromise;
// At this point, the page will be controlled by a service worker.
// You can start making requests at this point.
Note that for this use case, await navigator.serviceWorker.ready will not give you the behavior you need, as there can be a gap in time between when the navigator.serviceWorker.ready promise resolves and when the newly activated service worker takes control of the current page. You don't want that gap of time to lead to flaky tests.
I have this service worker:
//IMPORT POLYFILL
importScripts('cache-polyfill.js');
//INSTALL
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open('stock_item_balance_v1').then(function(cache) {
return cache.addAll([
'/app/offline_content/purchase/stock_items/stock_items_balance.php',
'/app/offline_content/purchase/stock_items/js/stock_items_balance.js'
]);
})
);
});
//FETCH (FETCH IS WHEN YOU CHECK FOR INTERNET)
self.addEventListener('fetch', function(event) {
//console.log(event.request.url);
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
In "stock_items_balance.php" i fetch data from my DB. So in every 30 minutes i would like to update my cached pages and reload the window.
So first i have a script that checks for internet connection.
If true, i want to clean/update the cache and reload the page.
How can i do that?
//INTERVAL
setInterval(function(){
//CLEAN/UPDATE CACHED FILES
serviceworker.update(); // ???
//RELOAD PAGE
window.location.reload();
}, 180000);
(I think you have a larger question as to whether the approach you describe is actually going to give a good, predictable, offline-capable experience for your users, but I'm just going to focus on the actual technical question you asked.)
Messaging the service worker
First off, you should keep in mind that it's possible to have multiple tabs open for the same URL, and if you, you're going to end up with your update code potentially running multiple times. The code in this answer handles the "reload" step for you from inside of the service worker, after the asynchronous cache update has completed, by getting a list of all the active clients of the service worker and telling each to navigate to the current URL (which is effectively a reload).
// Additions to your service worker code:
self.addEventListener('message', (event) => {
// Optional: if you need to potentially send different
// messages, use a different identifier for each.
if (event.data === 'update') {
event.waitUntil((async () => {
// TODO: Move these URLs and cache names into constants.
const cache = await caches.open('stock_item_balance_v1');
await cache.addAll([
'/app/offline_content/purchase/stock_items/stock_items_balance.php',
'/app/offline_content/purchase/stock_items/js/stock_items_balance.js'
]);
const windowClients = await clients.matchAll();
for (const windowClient of windowClients) {
// Optional: check windowClient.url first and
// only call navigate() if it's the URL for one
// specific page.
windowClient.navigate(windowClient.url);
}
})());
}
});
// Additions to your window/page code:
setInterval(() => {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('update');
}
}, 180000);
What won't work
The Cache Storage API is available from both inside a service worker and inside of your page's window scope. Normally what I'd recommend that folks do is to open up the same cache from the window context, and call cache.add() to update the cached entry with the latest from the network. However, calling cache.add() from the window context will cause the network request to be intercepted by your fetch handler, and at that point, your response won't actually come from the network. By calling cache.add() from inside your service worker, you can guarantee that the resulting network request won't trigger your fetch handler.
I'm trying to load a service worker before all subresource requests on the page so I can apply some optimizations to the way subresources are loaded (e.g. lazy-loading, loading minified versions of assets instead of full assets). However, I cannot find a way load my SW before other subresource requests begin.
I created a simple proof-of-concept of what I'm trying to do to 401 any requests handled by my Service Worker (just to make it easier to find when my SW begins handling requests).
Here's my HTML:
<!doctype html>
<head>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/dl-wps-sw.js', { scope: '/' }).then(function (registration) {
console.log('Service Worker registration successful with scope: ', registration.scope);
}, function (err) {
console.error(err);
});
}
</script>
...
and here's my Service Worker:
self.addEventListener('install', function (event) {
self.skipWaiting();
});
self.addEventListener('activate', () => {
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
const init = {status: 401, statusText: 'Blocked!'};
event.respondWith(new Response(null, init));
});
This is what happens in my browser:
As you can see in the screenshot, even though my code to register the Service Worker is at the very top of the page, it doesn't activate and begin handling requests until a bit later, and by then a large number of critical requests I need to catch have already fired.
I found someone else who seemed to be trying to do what I'm trying to accomplish (2 years earlier) in a Github issue for the Service Worker spec: https://github.com/w3c/ServiceWorker/issues/1282
It seems they suggested using a <link rel="serviceworker"... tag to do this, but it appears this link type has since been removed from Chrome for some reason: https://www.chromestatus.com/feature/5682681044008960
I've tried several other ideas to attempt to load my SW first:
Preloading my SW with a <link rel="preload"... tag
Preloading my SW with a fetch/XMLHttpRequest
Inlining the Service Worker in the HTML (not possible apparently)
Delaying the execution of the page by running a while loop for a few seconds (this kinda worked, but its a terrible unpredictable hack)
Any ideas or strategies I'm missing? My Google-fu has failed me on coming up with a solution.
You'll need to install the SW alone first, then refresh the page. The SW can then serve content for a SW-enabled page and intercept all other requests. Example: fetch-progress.anthum.com
index.html
<p>Installing Service Worker, please wait...</p>
<script>
navigator.serviceWorker.register('sw.js')
.then(reg => {
if (reg.installing) {
const sw = reg.installing || reg.waiting;
sw.onstatechange = function() {
if (sw.state === 'installed') {
// SW installed. Refresh page so SW can respond with SW-enabled page.
window.location.reload();
}
};
} else if (reg.active) {
// something's not right or SW is bypassed. previously-installed SW should have redirected this request to different page
handleError(new Error('Service Worker is installed and not redirecting.'))
}
})
.catch(handleError)
function handleError(error) {}
</script>
sw.js
self.addEventListener('fetch', event => {
const url = event.request.url;
const scope = self.registration.scope;
// serve index.html with service-worker-enabled page
if (url === scope || url === scope+'index.html') {
const newUrl = scope+'index-sw-enabled.html';
event.respondWith(fetch(newUrl))
} else {
// process other files here
}
});
index-sw-enabled.html
<!--
This page shows after SW installs.
Put your main app content on this page.
-->
I need a sample example of the service worker in Aurelia. I tried multiple code examples but no one was invoking the install, activate and fetch methods.
The service worker gets activated, but never got to the point where install is working or activated.
I need some help to understand the main issue.
Also I noticed that addEventListener "load" to the window is not responding therefore I used "aurelia-composed".
service-worker.js
// installing & Activating & fetching
self.addEventListener("install", event => {
console.log("====> service worker is installed ", event);
})
);
});
self.addEventListener("activate", event => {
console.log(" ====> service worker is activated", event);
});
self.addEventListener("fetch", event => {
console.log("====> service worker is fetched ", event);
});
index.ejs
// placing the service worker in HTML
<script>
if (navigator.serviceWorker) {
console.log("<== service worker supported ==>");
window.addEventListener("aurelia-composed", () => {
console.log("#### window loaded ######");
navigator.serviceWorker
.register("./service-worker.js")
.then(reg => {
console.log(" ++++ SW registered +++++++",reg);
})
.catch(err => {
console.log(` error in registering worker`,err);
});
});
}
</script>
there are two things I can observe for this issue:
1) Make sure the service-worker.js is outside the 'src' folder (where aurelia scans code). The service worker code must be a plain js file and must not be bundled.
Also, there is a potential syntax error in the service-worker.js file. Attached goes my own version:
self.addEventListener("install", event => {
console.log("====> service worker is installed ", event);
});
self.addEventListener("activate", event => {
console.log(" ====> service worker is activated", event);
});
self.addEventListener("fetch", event => {
console.log("====> service worker is fetched ", event);
});
2) The loading of the service worker file can be put at the end of the main.ts or main.js aurelia file, after the app is run:
await aurelia.start();
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(function (registration) { return registration.update(); })
.catch(console.error);
}
return aurelia.setRoot('app');
After doing all this, in my chrome console:
And voilá!
Best regards.
Hi Am facing a wired behavior in service worker. After clearing all the cache load the page service worker loading everything to Cache API. then i went offline and reload the page the page is not getting loaded. I went online and loaded the page again then went offline a load the page this time the page getting loaded correctly. i dont know why this behavior is it anything related to the wait time of service worker how to fix this.
After few debugging i found that my fetch code is not getting executed on very first page load. from second page load onward its getting the hit
my sample application hosted here https://ajeeshc.github.io/#/comments
My service worker file is available in here
complete demo code location here
Please help me out here am really in critical state.
I have few reading towards the delay in registering the service worker cause this issue how to fix this ?
below is my service worker registration code.
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('service-worker.js', { scope: './' })
.then(function (registration) {
console.log("Service worker registered - from the 24 ", registration)
registration.addEventListener('updatefound', () => {
newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
switch (newWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
showUpdateBar();
}
break;
}
});
});
})
.catch(function (err) {
console.log("Service Worker Failes to Register", err)
})
navigator.serviceWorker.addEventListener('message', function (event) {
document.getElementById("cache-generic-msg").style.display = "block";
console.log("Got reply from service worker: " + event.data);
});
let refreshing;
navigator.serviceWorker.addEventListener('controllerchange', function () {
if (refreshing) return;
window.location.reload();
refreshing = true;
});
}
after few research i found what i was missing here.
with my code at very first load the service worker not getting registered to client. That y its not able to invoke any fetch event. if you see the application tab you can check the client is registered or not. if it registered you will have the name there else it will be empty
Now how to register the client at first load itself use
self.clients.claim()
Please find the code below
self.addEventListener('activate', (event) => {
console.info('Event: Activate');
event.waitUntil(
self.clients.claim(),
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cache) => {
if (cache !== cacheName) {
return caches.delete(cache);
}
})
);
})
);
});