I'm having some problems on setting up a service worker for my website.
I only want to cache css/js/fonts and some images/svg, I don't want to cache the HTML since all of it is updated every minute.
It kinda works, but trying on my smartphone I keep getting the notification "Add to homescreen" even when I've already added it. And on the Chrome Dev app I don't get the Add button.
Also with the Lighthouse I get the following errors:
"Does not respond with a 200 when offline"
"User will not be prompted to Install the Web App, Failures: Manifest start_url is not cached by a Service Worker."
Right now my sw.js is like this. As you can see I commented the fetch part because it was caching the HTML and also the Cookies weren't working.
Is there around a simple Service Worker "template" to use?
const PRECACHE = 'app-name';
const RUNTIME = 'runtime';
// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
'/css/file.css',
'/js/file.js',
'/images/logo.png',
'/fonts/roboto/Roboto-Regular.woff2'
]
// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', event => {
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())
);
});
// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', event => {
const currentCaches = [PRECACHE, RUNTIME];
event.waitUntil(
caches.keys().then(cacheNames => {
return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
}).then(cachesToDelete => {
return Promise.all(cachesToDelete.map(cacheToDelete => {
return caches.delete(cacheToDelete);
}));
}).then(() => self.clients.claim())
);
});
// The fetch handler serves responses for same-origin resources from a cache.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
// if (event.request.url.startsWith(self.location.origin)) {
// event.respondWith(
// caches.match(event.request).then(cachedResponse => {
// if (cachedResponse) {
// return cachedResponse;
// }
// return caches.open(RUNTIME).then(cache => {
// return fetch(event.request).then(response => {
// // Put a copy of the response in the runtime cache.
// return cache.put(event.request, response.clone()).then(() => {
// return response;
// });
// });
// });
// })
// );
// }
});
I'm not sure why the install banner appears but the two errors given by lighthouse are related to the missing caching of the very start_url, propably index.html. So Lighthouse will always be telling you about those if you follow the caching strategy you described here.
I suggest you could try Workbox and their runtime caching. Runtime caching, in a nutshell, works like so: you specify urls like *.svg, *.css etc. and the SW caches them once the client first asks them. In the future, when the files are already cached, the SW serves them from the cache to the client. Basically you tell the SW to cache this and that kind of urls when it encounters them and not in advance.
Runtime caching could very well be accompanied by precaching (that may also be found from Workbox!) to cache a bunch of files.
Check it out here: https://workboxjs.org
They have a couple of examples and plugins for build tooling.
Related
I'm trying to setup PWA for my blazor application. I followed the instructions on: https://learn.microsoft.com/en-us/aspnet/core/blazor/progressive-web-app?view=aspnetcore-6.0&tabs=visual-studio
But when I open the deployed website the following error occurs:
Failed to find a valid digest in the 'integrity' attribute for resource 'domain/manifest.json' with computed SHA-256 integrity 'uDWnAIEnaz9hFx7aEpJJVS1a+QB/W7fMELDfHWSOFkQ='. The resource has been blocked.
Unknown error occurred while trying to verify integrity.
service-worker.js:22
Uncaught (in promise) TypeError: Failed to fetch
at service-worker.js:22:54
at async onInstall (service-worker.js:22:5)
In the source file this happens here:
async function onInstall(event) {
console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
I think the error is happening since the entry in assetsRequests has a wrong hash and the resource is blocked. If I remove the file from the service-worker-assets.js, the service worker installs and the PWA can be used. But I think this is not a reliable solution.
This also happens sometimes for the appsettings.json. In the service-worker-assets.js I can find the following entry:
{
"hash": "sha256-+Py0\/ezc+0k1sm\/aruGPrVhS1jOCTfPKMhOSS+bunek=",
"url": "manifest.json"
},
So the hash does not seem to match. Where does the browser take the wrong hash from? How can I fix this so it does match?
Also it seems that the app is caching older files sometimes. Even when I do a "Reset Cache & Hard Reload" in Chrome the service-worker.js file is still an older version. Any idea how to fix this as well, since it might be related?
Edit: I was also checking this solution: https://stackoverflow.com/a/69935118/11385442. But in the mentioned blazor.boot.json I cannot find any reference to the manifest.json or the appsettings.json. Only Dlls are listed. So the problem only seems to relate to files not listed in blazor.boot.json.
Edit2: What I can see on the webserver is that the following files are published:
appsettings.json
appsettings.json.br
appsettings.json.gzip
So it seems like compressed version are added. Also the appsettings.json has a different size than the one in the solution. My guess is that somewhere in the build or release pipeline (Azure) the files are modified. But even when I copy the appsettings.json manually to the webserver the error still occurs. I was following Information provided here: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly?view=aspnetcore-5.0
(Diagnosing integrity problems)
My guess was right. The appsettings.json was modified probably due to the xml transformation in the azure pipeline. My current solution is to exclude integrity validation for such resources as described in the following answer: Error loading appsettings.Production.json due to digest integrity issue
Also I changed the "sw-registrator.js" mentioned in the original posts comments to work correctly, because it didn't load the new files into the cache:
function invokeServiceWorkerUpdateFlow(registration) {
if (confirm("New version available, reload?") == true) {
if (registration.waiting) {
console.info(`Service worker registrator: Post skip_waiting...`);
// let waiting Service Worker know it should became active
registration.waiting.postMessage('SKIP_WAITING')
}
}
}
function checkServiceWorkerUpdate(registration) {
setInterval(() => {
console.info(`Service worker registrator: Checking for update... (scope: ${registration.scope})`);
registration.update();
}, 60 * 1000); // 60000ms -> check each minute
}
// check if the browser supports serviceWorker at all
if ('serviceWorker' in navigator) {
// wait for the page to load
window.addEventListener('load', async () => {
// register the service worker from the file specified
const registration = await navigator.serviceWorker.register('/service-worker.js');
// ensure the case when the updatefound event was missed is also handled
// by re-invoking the prompt when there's a waiting Service Worker
if (registration.waiting) {
invokeServiceWorkerUpdateFlow(registration);
}
// detect Service Worker update available and wait for it to become installed
registration.addEventListener('updatefound', () => {
if (registration.installing) {
// wait until the new Service worker is actually installed (ready to take over)
registration.installing.addEventListener('statechange', () => {
if (registration.waiting) {
// if there's an existing controller (previous Service Worker), show the prompt
if (navigator.serviceWorker.controller) {
invokeServiceWorkerUpdateFlow(registration);
} else {
// otherwise it's the first install, nothing to do
console.log('Service worker registrator: Initialized for the first time.')
}
}
});
}
});
checkServiceWorkerUpdate(registration);
let refreshing = false;
// detect controller change and refresh the page
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.info(`Service worker registrator: Refreshing app... (refreshing: ${refreshing})`);
if (!refreshing) {
window.location.reload();
refreshing = true
}
});
});
}
else
{
console.error(`Service worker registrator: This browser doesn't support service workers.`);
}
Also I had to add this in service-worker.js:
self.addEventListener('message', (event) => {
console.info('Service worker: Message received');
if (event.data === 'SKIP_WAITING') {
// Cause the service worker to update
self.skipWaiting();
}
});
This code was mostly taken from https://whatwebcando.today/articles/handling-service-worker-updates/
I am trying to set up my website to have a fallback page when it is loaded without an internet connection. To do that, I am following this guide on web.dev: "Create an offline fallback page"
I modified the example ServiceWorker in the article to fit my purposes, including being able to serve external CSS and images in the fallback offline page:
// Incrementing OFFLINE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "offline.html";
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
// Setting {cache: 'reload'} in the new request will ensure that the response
// isn't fulfilled from the HTTP cache; i.e., it will be from the network.
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
await cache.add(new Request("offline.css", { cache: "reload" }));
await cache.add(new Request("logo.png", { cache: "reload" }));
await cache.add(new Request("unsupportedCloud.svg", { cache: "reload" }));
})()
);
});
self.addEventListener("activate", (event) => {
// Tell the active service worker to take control of the page immediately.
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page.
if (event.request.mode === "navigate") {
if (event.request.url.match(/SignOut/)) {
return false;
}
event.respondWith(
(async () => {
try {
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log("Fetch failed; returning offline page instead.", error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})()
);
}
});
However, when the offline.html page loads it does is unable to load the images and the CSS; the images fail to load with a 404 error and the request for the CSS doesn't even show in the Network tab of the browser dev console.
I would expect the images and CSS to be fetched from the ServiceWorker cache, but it seems that neither is.
Am I missing something on how ServiceWorkers cache requests or how they fetch them? Or on how to design the offline fallback page to work?
Turns out there were a few reasons why the assets were not being found.
The first reason is because when they were saved to cache, they were saved with the entire path where they are stored alongside the Service Worker file.
So the path that was saved was something along the lines of static/PWA/[offline.css, logo.png, unsupportedCloud.svg] but the path of the page that requested them was in the root. In offline.html I had to reference them as such: <img src="static/PWA/unsupportedCloud.svg" class="unsupported-cloud" />.
The second reason is that the Service Worker only checks for fetch events were of type navigation. In my example you can see I had written if (event.request.mode === "navigate") {...} so we only attempted to use the cache that we set up in navigation events, which would not catch fetch events to get assets. To fix that, I set up a new check for no-cors event modes: else if (event.request.mode === "no-cors") {...}.
These two fixes let me get assets from the offline cache that I set up on Service Worker installation. With some other minor fixes, this addresses my question!
I have a PWA made with HTML, CSS and JS with Node.js, everytime I change the styles.css of the app, I have to upload it again, I.e. change the port. For example in localhost:3000 it would have the old styling, but if I upload it to localhost:3100, the styling changed to the new one, how can I make it so that cached css files will be deleted and uploaded with the new ones?
This is my service worker:
var CACHE_NAME = 'version-1'; // bump this version when you make changes.
// Put all your urls that you want to cache in this array
var urlsToCache = [
'index.html',
'assets/logo-192.png',
'images/airplane.png',
'images/backspace.png',
'images/calcToggle.png',
'images/diamond.png',
'images/favicon.png',
'images/hamburger.png',
'images/history.png',
'images/like.png',
'images/love.png',
'images/menu2.png',
'images/menu3.png',
'images/menu4.png',
'images/menu5.png',
'images/menu6.png',
'images/menu7.png',
'images/menu8.png',
'images/plane.png',
'images/science.png',
'images/settings.png',
'images/trash.png',
'styles.css'
];
// Install the service worker and open the cache and add files mentioned in array to cache
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
// keep fetching the requests from the user
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) return response;
return fetch(event.request);
})
);
});
self.addEventListener('activate', function(event) {
var cacheWhitelist = []; // add cache names which you do not want to delete
cacheWhitelist.push(CACHE_NAME);
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (!cacheWhitelist.includes(cacheName)) {
return caches.delete(cacheName);
}
})
);
})
);
});
If you are doing this for development just open your dev tools. Select the application tab, then the service worker panel.
Click the 'Bypass for Network' option.
I wrote an article on service worker dev best practices that might help:
https://love2dev.com/serviceworker/development-best-practices/
If you need to update in production that is different. I generally do a periodic HEAD request on the network asset (CSS file in your example). If the resource is newer than the cached version I update to the latest version as needed.
I have other techniques I use from time to time as well. It varies by application and the requirements, etc.
I transformed my React application into a PWA and it is working partially fine.
I followed this tutorial: https://medium.com/#toricpope/transform-a-react-app-into-a-progressive-web-app-pwa-dea336bd96e6
However this article only shows how to cache static data and I also need to store data stemming from the server, I could do this following the instruction of the first answer of this post: How can I cache data from API to Cache Storage in React PWA? and inserting the firebase adresses where the data is stored into the array urlsToCache, populated by the files that should be stored into the cache.
So far so good, however after the data be stored into the cache, the application stops fetching data from the server and loads the page only with data from the cache, even if the server is updated. This is what I need to fix.
In short, I need to fetch the data from the server, store it into the cache in order to use it when the application is offline and update the cache every time the server is reached.
I am trying to follow this guide, but without success: https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#serving-suggestions
This is my worker.js file:
var CACHE_NAME = 'pwa-task-manager';
var urlsToCache = [
'/',
'/completed',
'/index.html',
'/static/js/main.chunk.js',
'/static/js/0.chunk.js',
'/static/js/1.chunk.js',
'/static/js/bundle.js',
'/calculadora',
'https://calc-marmo.firebaseio.com/clientes.json',
'https://calc-marmo.firebaseio.com/adm.json',
];
// Install a service worker
self.addEventListener('install', event => {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
// Cache and return requests
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
// Update a service worker
self.addEventListener('activate', event => {
var cacheWhitelist = ['pwa-task-manager'];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
Any help would be much appreciated.
This sound like you need a Network First strategy which is not mention in the cookbook. This strategy is similar to Network falling back to cache but additionally stores the response always in the cache.
Explanation: https://developers.google.com/web/tools/workbox/modules/workbox-strategies#network_first_network_falling_back_to_cache
Code sample (if you don't use workbox): https://gist.github.com/JMPerez/8ca8d5ffcc0cc45a8b4e1c279efd8a94
While testing service workers for a project and using this example from google:
/*
Copyright 2016 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Names of the two caches used in this version of the service worker.
// Change to v2, etc. when you update any of the local resources, which will
// in turn trigger the install event again.
const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime';
// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
'index.html',
'./', // Alias for index.html
'styles.css',
'../../styles/main.css',
'demo.js'
];
// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', event => {
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())
);
});
// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', event => {
const currentCaches = [PRECACHE, RUNTIME];
event.waitUntil(
caches.keys().then(cacheNames => {
return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
}).then(cachesToDelete => {
return Promise.all(cachesToDelete.map(cacheToDelete => {
return caches.delete(cacheToDelete);
}));
}).then(() => self.clients.claim())
);
});
// The fetch handler serves responses for same-origin resources from a cache.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
if (event.request.url.startsWith(self.location.origin)) {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return caches.open(RUNTIME).then(cache => {
return fetch(event.request).then(response => {
// Put a copy of the response in the runtime cache.
return cache.put(event.request, response.clone()).then(() => {
return response;
});
});
});
})
);
}
});
source: https://github.com/GoogleChrome/samples/blob/gh-pages/service-worker/basic/service-worker.js
I discovered that Firefox (in contrast to safari and chrome) throws errors within event.waitUntil() as well as event.respondWith() if something does not work with the fetch requests (even if its just a 206 partial content response):
Service worker event waitUntil() was passed a promise that rejected
with 'TypeError: Cache got basic response with bad status 206 while
trying to add request
That behaviour breaks the installer. If I add a .catch() to the installer like this
self.addEventListener('install', event => {
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())
.catch(function(err){
console.log(err);
self.skipWaiting();
})
);
});
I presume the first 206 will make the precache stop (?)
Also after that the sw gets installed but once in a while I get a
Service worker event respondWith() was passed a promise that rejected
with 'TypeError: Cache got basic response with bad status 206 while
trying to add request
and even if that does not happen, if I try to open the a link to the url that threw the 206 error while installation/precaching I get:
Failed to load ‘https://xxxx/yyyyy.mp3’. A
ServiceWorker passed a promise to FetchEvent.respondWith() that
rejected with ‘TypeError: Cache got basic response with bad status 206
while trying to add request
https://xxxx/yyyyy.mp3’.
how can I handle this kind of error properly? catching like above doesn't make much sense for it breaks the forced precaching. And even if that would be acceptable, it seems to interfere with every request happening from then on and might cause trouble later on.
one half of the problem i could solve by moving the return statement from within the cache.put() function outside of it:
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
if (event.request.url.startsWith(self.location.origin)) {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return caches.open(RUNTIME).then(cache => {
return fetch(event.request).then(response => {
// Put a copy of the response in the runtime cache.
cache.put(event.request, response.clone()).then(() => {
console.log("logged a file into RUNTIME:");
console.log(response);
});
return response; // and return anyhow whatever came back
});
});
})
);
}
});
this way the sw does not wait for the cache.put() to be successful and yet it gets cached most of the times.
this solves the most urgent issue but problems still are
a) forced precaching still gets cancelled by 206 responses
b) in case I would want to make sure that requests are cached in runtime, i would still need to make some retry() function or sth.