Service worker automatic update process - javascript

I have a doubt about the service worker update process.
In my project there are 2 files related to sw:
"sw.js", placed in website root, will be NOT cached (by Cache API and Web Browser).
My service worker manages the cache of all statics files and all dynamic url pages.
Sometimes I need to update it and the client must detect that there's an update and do that immediatelly.
"sw_main.js" is the script that installs my sw. This file is cached by Cache API because my app must work offline.
Inside we can find:
var SW_VERSION = '1.2';
navigator.serviceWorker.register("sw.js?v=" + SW_VERSION, { scope: "/" }).then(....
The problem is: because sw_main.js is cached, if I change the SW_VERSION and then deploy online the webapp, all clients will not
update because cannot see the changes in that file.
Which is the best way to manage the SW update process?
As I now, there are 3 ways to trigger sw update:
push and sync events (but I'm not implementing these)
calling .register() only if the service worker URL has changed (but
in my case it's not possible because the sw_main.js is cached so I'm
not able to change the SW url)
navigation to an in-scope page (I think we've the same cache problem
of point 2)
I read also this: "Your service worker is considered updated if it's byte-different to the one the browser already has".
That means that if I change the content of sw.js (that is not cached), the service worker will automatically detect the update?
Thank you

I found 2 possibile solutions.
First of all I wanna say that it's better to cache (using pwa Cache API) also the sw.js because when you're offline, it will be requested by sw_main.js.
FIRST SOLUTION:
Use a the service worker's cache as a fallback and always attempt to go network-first via a fetch().
This only for sw.js and maybe sw_main.js.
You lose some performance gains that a cache-first strategy offers, but the js file size is very light so I don't think it's a big problem.
SECOND SOLUTION:
If your cached sw.js file has changed?
We can hook into "onupdatefound" function on the registered Service Worker.
Even though you can cache tons of files, the Service Worker only checks the hash of your registered service-worker.js.
If that file has only 1 little change in it, it will be treated as a new version.
So this confirm my previous question! I'll try it!
If it works, the second solution is the best

Related

How to update service workers?

Situation:
On mywebsite.com/game, I registered a service-worker with
navigator.serviceWorker.register('/service-worker.js', {scope: "/"});
On my server, '/service-worker.js' has a maxAge of 1d.
Problem:
service-worker.js has a major bug. It always displays an empty page and can't fetch anything. service-worker.js must be changed.
The problem is whenever a user goes to mywebsite.com/game, it displays the empty page and does nothing more. I am unable to make the client fetch the new service-worker.js.
How can I make the client fetch the new service-worker.js?
What you're describing—a check for updates to /service-worker.js—happens by default, automatically, under the circumstances laid out in this article:
An update is triggered if any of the following happens:
A navigation to an in-scope page.
A functional events such as push and sync, unless there's been an update check within the previous 24 hours.
Calling .register() only if the service worker URL has changed. However, you should avoid changing the worker URL.
All modern web browsers will ignore any Cache-Control headers you set on /service-worker.js by default and go directly against the web server to obtain the latest copy.
This Stack Overflow answer has some best practices for what the revised service-worker.js file should contain if you want it to behave like a "kill switch."
Just add ?v=1 to your script like this.
navigator.serviceWorker.register('/service-worker.js?v=1', {scope: "/"});
And increment the number of script version when you make changes of service worker's script

How to make Angular Universal and PWA work together?

I have a SSR Angular app which I am trying to transform into a PWA. I want it to be server-side rendered for SEO and for the "fast first rendering" that it provides.
The PWA mode works fine when combined with SSR, but once the app is loaded, when we refresh it, the client index HTML file is loaded instead of the server-side rendered page.
I have dug into the code of ngsw-worker.js and I saw this:
// Next, check if this is a navigation request for a route. Detect circular
// navigations by checking if the request URL is the same as the index URL.
if (req.url !== this.manifest.index && this.isNavigationRequest(req)) {
// This was a navigation request. Re-enter `handleFetch` with a request for
// the URL.
return this.handleFetch(this.adapter.newRequest(this.manifest.index), context);
}
I have no control over this file since it's from the framework and not exposed to developers.
Did anybody find a solution or workaround for this?
Up-to-date answer (v11.0.0)
Angular now has a navigationRequestStrategy option which allows to prioritize server requests for navigation. Extract of the changelog:
service-worker: add the option to prefer network for navigation
requests (#38565) (a206852), closes #38194
To be used wisely! This warning appears in the documentation:
The freshness strategy usually results in more requests sent to the
server, which can increase response latency. It is recommended that
you use the default performance strategy whenever possible.
Old answer (for archaeological purposes)
I have found a working solution, the navigationUrls property of ngsw-config.json contains a list of navigation URLs included or excluded (with an exclamation mark) like explained in the documentation.
Then I configured it like this:
"navigationUrls": [
"!/**"
]
This way, none of the URLs redirect to index.html and the server-side rendered app comes into play when the app is first requested (or refreshed), whatever the URL is.
To go further, the three kinds of URLs managed by the service worker are:
Non-navigation URLs: static files cached by the service worker and listed in the generated ngsw.json file with their corresponding hashes
Navigation URLs: redirected to index.html by default, forwarded to the server if the "!/**" configuration is used
GET requests to the backend: forwarded to the backend
In order to distinguish a GET XMLHttpRequest from a navigation request, the service worker uses the Request.mode property and the Accept header that contains text/html when navigating and application/json, text/plain, */* when requesting the backend.
Edit: This is actually not a good practice to do that for two reasons:
Depending on the network quality, there is no guarantee that the server-side version will render faster than the cached browser version
It breaks the "update in background" mechanism. Indeed, the server-side rendered app will always refer to the latest versions of the JavaScript files
For more details on this, please take a look at the Angular's team member answer to my feature request: https://github.com/angular/angular/issues/30861

What are the consequences of unregistering the service worker in create-react-app project?

I have a create-react-app project and I am running into an issue when there is a new production release the users don't get the latest version unless the clear the cache or in the best scenario when refreshing the page after they access it.
My Cloudflare cache expiry is set to 4 hours, but obviously the users still get the old version after this period. This leaves me thinking that it is a service worker issue.
Are there any other reasons that lead to this behaviour?
What are the possible solutions?
Is unregistering the SW considered a good solution for this issue? Knowing that I don't need my app to run offline at the moment.
If it is a good solution what are the consequences of unregistering it?
Do I need to use cache-control headers (ie max-age=0) in my index.html?
I know it is a lot of questions, but I wanted to show the directions I am thinking of and the areas I am bit confused about.
Thank you for your time and help in advance.
Adding versioning to your service worker cache is one of the ways you can ensure the new service worker gets installed whenever there is a new build. Just add a script which increments the version of the cache with each new build which causes a byte difference in service worker which in enough for the browser to trigger the new install event.
In your service worker file add something like
const version = 1;
let cacheV = 'foo' + version;
In your Activate event add logic that if there is a version mismatch delete the old cache.
self.addEventListener("activate", function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheV!== cacheName && cacheName.startsWith("foo")) {
return caches.delete(cacheName);
}
})
);
})
);
});
Also you can add update logic to your fetch listener which will fetch the latest file from network like
event.waituntil(update(request));

Force Service Worker to only return cached responses when specific page is open

I have built a portal which provides access to several features, including trouble ticket functionality.
The client has asked me to make trouble ticket functionality available offline. They want to be able to "check out" specific existing tickets while online, which are then accessible (view/edit) while the user's device is out-of-range of any internet connection. Also, they want the ability to create new tickets while offline. Then, when the connection is available, they will check in the changed/newly created tickets.
I have been tinkering with Service Workers and reviewing some good documentation on them, and I feel I have a basic understanding of how to cache the data.
However, since I only want to make the Ticketing portion of the portal available offline, I don't want the service worker caching or returning cached data when any other page of the portal is being accessed. All pages are in the same directory, so the service worker, once loaded, would by default intercept all requests from all pages in the portal.
How can I set up the service worker to only respond with cached data when the Tickets page is open?
Do I have to manually check the window.location value when fetch events occur? For example,
if (window.location == 'https://www.myurl.com/tickets')
{
// try to get the request from network. If successful, cache the result.
// If not successful, try returning the request from the cache.
}
else
{
// only try the network, and don't cache the result.
}
There are many supporting files that need to be loaded for the page (i.e. css files, js files, etc.) so it's not enough to simply check the request.url for the page name. Will 'window.location' be accessible in the service worker event, and is this a reasonable way to accomplish this?
Use service worker scoping
I know that you mentioned that you currently have all pages served from the same directory... but if you have any flexibility over your web app's URL structure at all, then the cleanest approach would be to serve your ticket functionality from URLs that begin with a unique path prefix (like /tickets/) and then host your service worker from /tickets/service-worker.js. The effort to reorganize your URLs may be worthwhile if it means being able to take advantage of the default service worker scoping and just not have to worry about pages outside of /tickets/ being controlled by a service worker.
Infer the referrer
There's information in this answer about determining what the referring window client URL is from within your service worker's fetch handler. You can combine that with an initial check in the fetch handler to see if it's a navigation request and use that to exit early.
const TICKETS = '/tickets';
self.addEventListener('fetch', event => {
const requestUrl = new URL(event.request.url);
if (event.request.mode === 'navigate' && requestUrl.pathname !== TICKETS) {
return;
}
const referrerUrl = ...; // See https://stackoverflow.com/questions/50045641
if (referrerUrl.pathname !== TICKETS) {
return;
}
// At this point, you know that it's either a navigation for /tickets,
// or a request for a subresource from /tickets.
});

How to cache bust sw-toolbox?

I have been toying around with service workers and sw-toolbox. Both are great methods but seems to have their weaknesses.
My project started out using Google's method of service workers (link). The way I see this is that you have to manually update the version number for cache busting. I could be wrong also but I don't think the pages that the users has visited will not be cached.
Compared to the sw-toolbox method, all I need to add is the following code:
self.toolbox.router.default = self.toolbox.networkFirst;
self.toolbox.router.get('/(.*)', function (req, vals, opts) {
return self.toolbox.networkFirst(req, vals, opts)
.catch(function (error) {
if (req.method === 'GET' && req.headers.get('accept').includes('text/html')) {
return self.toolbox.cacheOnly(new Request(OFFLINE_URL), vals, opts);
}
throw error;
});
});
Then the problem of caching pages will be solved. Here is my issue: after applying the sw-toolbox to my project, the old service worker doesn't get cleared or replaced by the new one unless I go to the dev tools to clear it.
Any ideas how to get around this?
Here is my issue: after applying the sw-toolbox to my project, the old
service worker doesn't get cleared or replaced by the new one unless I
go to the dev tools to clear it.
The browser checks for updates to the service worker file every time it requests a resource in the service worker scope. If there is a byte difference in the service worker files, the browser will install the new service worker. You only need to update the service worker manually in dev tools because the app is still running, and the browser does not want to activate a new service worker while the old one is still in use.
If you close all pages associated with the service worker (like a user would when leaving your app), the browser will be able to activate the new service worker the next time your page is opened.
If you want to force the new service worker to take over, you can add self.skipWaiting(); to the install event. Here is some documentation with an example.
You can learn just about everything you need to know about the service worker's life cycle from this post by Jake Arichbald.
As far as caching & cache management goes, tools like sw-toolbox will handle cache busting for you. And actually, Workbox is a new tool that is meant to replace sw-toolbox & sw-precache. It will also handle cache busting and cache management (by comparing file hashes & setting/tracking resource expiration dates).
Generally speaking, you should always use a tool like Workbox to write your service workers. Writing them by hand is error prone and you are likely to miss corner cases.
Hope that helps.
P.S. If you end up not using skipWaiting and instead only updating when the page is closed & re-opened by a user, you can still enable automatic updating for development. In Chrome's dev tools, Application > Service Workers has an Update on reload option to automatically update the service worker.
I don't know if sw_toolbox has cache busting built in. Typically when you change the service worker and need to purge the previous version's cache you should do that with in the activate event handler.
The best practice here is to name your caches with the sw version number included. Here is some example code from an online course I have on service worker caching that might get you started:
self.addEventListener("activate", event => {
console.log("service worker activated");
//on activate
event.waitUntil(caches.keys()
.then(function (cacheNames) {
cacheNames.forEach(function (value) {
if (value.indexOf(config.version) < 0) {
caches.delete(value);
}
});
return;
})
);
});

Categories

Resources