React App with Service worker - Force update cache - javascript

I am trying different approaches to clearing the browser cache on application update.
This has proven to be more difficult than expected. :)
My refresh on new version code looks like this.
My app mounts in app.js
I have this code in serviceWorker-base.js (I am using workbox)
addEventListener('message', function(messageEvent){
if (messageEvent.data === 'skipWaiting') return skipWaiting();
});
My app.js
// skipWaiting() functions
function promptUserToRefresh(registration) {
// this is just an example - don't use window.confirm in real life; it's terrible
if (window.confirm("New version available! Refresh?")) {
registration.waiting.postMessage('skipWaiting')
}
}
function listenForWaitingServiceWorker(registration) {
console.log('listenForWaitingServiceWorker', registration)
function awaitStateChange() {
registration.installing.addEventListener('statechange', function() {
if (this.state === 'installed') promptUserToRefresh(registration)
})
}
if (!registration) return
if (registration.waiting) return promptUserToRefresh(registration)
if (registration.installing) awaitStateChange()
registration.addEventListener('updatefound', awaitStateChange)
}
//**
const enableServiceWorker = true
const serviceWorkerAvailable = ('serviceWorker' in navigator) ? true : false
// Register service worker
if (enableServiceWorker && serviceWorkerAvailable) {
navigator.serviceWorker.register('/serviceWorker.js')
.then( (registration) => {
console.log('Service worker registered', registration)
listenForWaitingServiceWorker(registration) // ** skipWaiting() code
})
}
// Install prompt event handler
export let deferredPrompt
window.addEventListener('beforeinstallprompt', (event) => {
// Prevent Chrome 76 and later from showing the mini-infobar
event.preventDefault()
deferredPrompt = event // Stash the event so it can be triggered later.
try{
showInstallPromotion()
}catch(e){
console.error('showInstallPromotion()', e)
}
})
window.addEventListener('appinstalled', (event) => {
console.log('a2hs installed')
})
My question is - Would it be possible to add a function to clear the browser cache when the user clicks "OK"?
Something along the lines of:
const refreshCacheAndReload = () => {
if (caches) {
// Service worker cache should be cleared with caches.delete()
caches.keys().then((names) => {
for (const name of names) {
caches.delete(name);
}
});
}
// delete browser cache and hard reload
window.location.reload(true);
};
Or are there better ways to force the browser to clear the cache?
Kind regards/K
UPDATE
The reason for wanting to clear the cache is to force the client to receive a new version of the application when a new release is deployed. (bundle files, css-files, etc.) As of today this requires hard reload and/or several reloads. Sometimes the application only receives parts of the new application on first reload.

Related

How to update cache in PWA (No Framework)

Am working on PWA and am trying to make my App works offline, so i cache my assets and its works fine, i could even use my app through my cached assets, BUT the problem is whenever i delete my cache using dev tools or whenever i update something in my assets my app not regenerate the cache again, how to regenrate the cache everytime when i start using the app online
Service worker registeration (app.js):
if('serviceWorker' in navigator){
navigator.serviceWorker.register('./sw.js')
.then((reg) => console.log('Service worker registered', reg))
.catch((err) => console.log('Service worker not registered', err));
}
My service worker looks like this:
const cacheName = 'VSCode_SG_' + Date.now(),
assets = [
'./',
'./app.min.js',
'./favicon.ico',
'./style.css',
];
self.addEventListener('install', evt => {
evt.waitUntil(caches.open(cacheName).then(cache => {
cache.addAll(assets);
}))
});
self.addEventListener('activate', evt => {
// Get all the currently active `Cache` instances.
evt.waitUntil(caches.keys().then((keys) => {
// Delete all caches that aren't in the allow list:
return Promise.all(keys.map((key) => {
if (!cacheName.includes(key)) {
return caches.delete(key);
}
}));
}));
});
self.addEventListener('fetch', evt => {
evt.respondWith(
caches.match(evt.request).then(cacheRes => {
return cacheRes || fetch(evt.request);
}).catch(() => console.log('An error has occurred'))
);
});
am sure that am missing a small detail to solve the problem but i don't have any clues, anyone could help?
I think your issue might be related to your caches name, you want it to stay a static name instead of labeling it with a date stamp
const cacheName = 'VSCode_SG_STATIC', // keep it a constant string
assets = [
'./',
'./app.min.js',
'./favicon.ico',
'./style.css',
];
Give this a shot and wait until your new service worker is installed to try again

Prompting only once when there are multiple workbox-broadcast-update messages

I'm using Workbox and the BroadcastUpdatePlugin() in my serviceWorker to prompt the user to refresh the page when a cached file is updated. It works great when there's only one file updated, but when I publish multiple updates at once (HTML, CSS and JS files), the user is prompted to refresh the page for each file.
How can I update all files in the cache, then prompt the user to refresh the page only once, when the event listener has stopped receiving update messages?
ServiceWorker code
const {BroadcastUpdatePlugin} = workbox.broadcastUpdate;
registerRoute(
({request}) => request.destination === 'document',
new StaleWhileRevalidate({
//new NetworkOnly({
cacheName: 'pages',
plugins: [
new BroadcastUpdatePlugin(),
],
})
)
registerRoute(
({request}) => request.destination === 'script' || request.destination === 'style',
new StaleWhileRevalidate({
//new NetworkOnly({
cacheName: 'assets',
plugins: [
new BroadcastUpdatePlugin(),
],
})
)
JavaScript code
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js', { scope: '/' }).then(swReg => {
console.log('Service Worker Registered', swReg);
}).catch(error => {
console.log('There was an error!', error);
})
})
// Listen for cache updates and prompt a page reload
navigator.serviceWorker.addEventListener('message', async (event) => {
if (event.data.meta === 'workbox-broadcast-update') {
const {cacheName, updatedURL} = event.data.payload;
const cache = await caches.open(cacheName);
const updatedResponse = await cache.match(updatedURL);
const updatedText = await updatedResponse.text();
console.log('Updated: '+cacheName+', '+updatedURL);
// prompts for every update
if(confirm('Content Updated. Please refresh the page.')){
window.location.reload;
}
}
})
}
I could imagine there potentially being logic in your service worker that could help with this, by, for instance, looking at the cliendId in the FetchEvent that triggered an update and only conditionally sending the message if it hasn't seen that clientId before.
But that would require a bunch of custom logic beyond what BroadcastUpdatePlugin provides already, and it might be error proneā€”if your service worker broadcasts an update, then the user acts on the update, and then there's another update that could be broadcast, you probably want to give the user another chance to act on it, but it would be difficult for the service worker to know whether to broadcast a message to the same clientId in that scenario.
A cleaner approach would be to move the logic that prevented multiple prompt to the window client code.
There are a couple of ways to go there, but what seems the cleanest to me would rely on the resolution of a promise to trigger the prompt. You can call the resolve function of a promise multiple times, but the then() in the promise chain will only be invoked once, giving you the behavior you're looking for.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
const waitForUpdate = new Promise((resolve) => {
navigator.serviceWorker.addEventListener('message', async (event) => {
if (event.data.meta === 'workbox-broadcast-update') {
const {cacheName, updatedURL} = event.data.payload;
const cache = await caches.open(cacheName);
const updatedResponse = await cache.match(updatedURL);
const updatedText = await updatedResponse.text();
console.log(`Updated ${updatedURL} in ${cacheName}: ${updatedText}`);
// Calling resolve() will trigger the promise's then() once.
resolve();
}
});
});
waitForUpdate.then(() => {
if (confirm('Content updated. Please refresh the page.')) {
window.location.reload();
}
});
}
(You could also use an approach that relied on a global variable or something similar that gets flipped the first time you show the prompt, and short-circuits the prompt display every subsequent time, but I like using promises for this sort of thing.)

Updating service worker to Workbox 5 without bundler

I'm running a pretty straight forward PWA with Workbox 3, primarily for caching and offline purpose. The web page is a forum were users can install a PWA. Planning a upgrade were the current workbox 3 gives some errors when testing and therefor I had to re-build the service worker. Thought of giving workbox 5 a chance.
The code below is what I'm testing with today and purpose should be (pretty straight forward):
Give the user a chance to "install" the new service worker with a button (taken from: https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68)
Cache static assets but not html (except offline.html).
Give navigation preload a chance to increase performance.
Create a service worker based on Workbox 5 that is easy to update in the future (push messages).
service-worker.js:
// Load WB locally, skip preload of googleapis in header. (Version 5.1.3)
importScripts('/workbox/workbox-sw.js');
workbox.setConfig({
modulePathPrefix: '/workbox/'
});
// How are we doing?
if (workbox) {
console.log('Workbox loaded correctly');
} else {
console.log('Workbox did not load, check log');
}
/*
// Debug on or off, off in production
workbox.setConfig({
debug: true
});
*/
// A new SW is waiting, user clicks button that activates the new SW
addEventListener('message', e => {
if (e.data === 'skipWaiting') {
skipWaiting();
clientsClaim();
}
});
// Cache Offline page
const CACHE_NAME = 'offline-html';
const FALLBACK_HTML_URL = '/offline.html';
addEventListener('install', async (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.add(FALLBACK_HTML_URL))
);
});
// Start navigation preload to speed things up a bit.
workbox.navigationPreload.enable();
const networkOnly = new workbox.strategies.NetworkOnly();
const navigationHandler = async (params) => {
try {
// Attempt a network request.
return await networkOnly.handle(params);
} catch (error) {
// If it fails, return the cached HTML and log the error
console.log(error);
return caches.match(FALLBACK_HTML_URL, {
cacheName: CACHE_NAME,
});
}
};
// Register this strategy to handle all navigations.
const navigationRoute = new workbox.routing.NavigationRoute(navigationHandler);
workbox.routing.registerRoute(navigationRoute);
// Cache static assets
const {StaleWhileRevalidate} = workbox.strategies;
const {CacheFirst} = workbox.strategies;
const {CacheableResponsePlugin} = workbox.cacheableResponse;
workbox.routing.registerRoute(
({request}) => request.destination === 'script' || request.destination === 'style' || request.destination === 'font' || request.destination === 'manifest',
new StaleWhileRevalidate({
// Use a custom cache name.
cacheName: 'static-cache2',
})
);
// Cache image files.
workbox.routing.registerRoute(
({request}) => request.destination === 'image',
// Use the cache if it's available.
new CacheFirst({
// Use a custom cache name.
cacheName: 'image-cache2',
plugins: [
new workbox.expiration.ExpirationPlugin({
// Cache only 100 images.
maxEntries: 100,
// Cache for a maximum of two weeks.
maxAgeSeconds: 14 * 24 * 60 * 60,
purgeOnQuotaError: true,
})
],
})
);
// Try to cache opaque from CDN
workbox.routing.registerRoute(
({url}) => url.origin === 'https://cdn.mycdn.com' &&
url.pathname.startsWith('/static/'),
new CacheFirst({
cacheName: 'cdn-cache',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
})
]
})
);
Client JS:
function showRefreshUI(registration) {
// TODO: Display a toast or refresh UI.
// This demo creates and injects a button.
var button = document.createElement('button');
button.style.position = 'absolute';
button.style.bottom = '24px';
button.style.left = '24px';
button.textContent = 'A new version of the web app is waiting, click here to install';
button.addEventListener('click', function() {
if (!registration.waiting) {
// Just to ensure registration.waiting is available before
// calling postMessage()
return;
}
button.disabled = true;
registration.waiting.postMessage('skipWaiting');
});
document.body.appendChild(button);
};
function onNewServiceWorker(registration, callback) {
if (registration.waiting) {
// SW is waiting to activate. Can occur if multiple clients open and
// one of the clients is refreshed.
return callback();
}
function listenInstalledStateChange() {
registration.installing.addEventListener('statechange', function(event) {
if (event.target.state === 'installed') {
// A new service worker is available, inform the user
callback();
}
});
};
if (registration.installing) {
return listenInstalledStateChange();
}
// We are currently controlled so a new SW may be found...
// Add a listener in case a new SW is found,
registration.addEventListener('updatefound', listenInstalledStateChange);
}
window.addEventListener('load', function() {
var refreshing;
// When the user asks to refresh the UI, we'll need to reload the window
navigator.serviceWorker.addEventListener('controllerchange', function(event) {
if (refreshing) return; // prevent infinite refresh loop when you use "Update on Reload"
refreshing = true;
console.log('Controller loaded');
window.location.reload();
});
navigator.serviceWorker.register('/service-worker.js')
.then(function (registration) {
// Track updates to the Service Worker.
if (!navigator.serviceWorker.controller) {
// The window client isn't currently controlled so it's a new service
// worker that will activate immediately
return;
}
registration.update();
onNewServiceWorker(registration, function() {
showRefreshUI(registration);
});
});
});
This code works on my dev server, my questions to SO:s workbox gurus are if there are any pitfalls with it and maybe someone could share suggestions on how to make it better?
Service workers and Workbox are complex and my main concern is that I've built the service worker so it does not optimize against workbox and maybe uses wrong or bad code/functions/order of functions.

How can I force service worker to clear cache? [duplicate]

So, I have an HTML page with service worker,
the service worker cache the index.html and my JS files.
The problem is when I change the JS, the change doesn't show up directly on the client browser. Of course in chrome dev-tools, I can disable cache. But in chrome mobile, how do I do that?
I tried to access the site settings and hit the CLEAR % RESET button.
But it still loads the old page/load from cache.
I tried to use other browser or chrome incognito and it loads the new page.
Then, I try to clear my browsing data (just cache) and it works.
I guess that's not how it should work right? my user won't know if the page is updated without clearing the chrome browser cache.
If you know the cache name you can simply call caches.delete() from anywhere you like in the worker:
caches.delete(/*name*/);
And if you wanted to wipe all caches (and not wait for them, say this is a background task) you only need to add this:
caches.keys().then(function(names) {
for (let name of names)
caches.delete(name);
});
Use this to delete outdated caches:
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
// Return true if you want to remove this cache,
// but remember that caches are shared across
// the whole origin
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
});
Typically you update the CACHE_NAME in your service workers JS file so your worker installs again:
self.addEventListener('install', evt => {
evt.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(inputs))
)
})
Alternatively, to clear the cache for a PWA find the cache name:
self.caches.keys().then(keys => { keys.forEach(key => console.log(key)) })
then run the following to delete it:
self.caches.delete('my-site-cache')
Then refresh the page.
If you see any worker-related errors in the console after refreshing, you may also need to unregister the registered workers:
navigator.serviceWorker.getRegistrations()
.then(registrations => {
registrations.forEach(registration => {
registration.unregister()
})
})
The most elegant solution, with async/await:
const cacheName = 'v2';
self.addEventListener('activate', event => {
// Remove old caches
event.waitUntil(
(async () => {
const keys = await caches.keys();
return keys.map(async (cache) => {
if(cache !== cacheName) {
console.log('Service Worker: Removing old cache: '+cache);
return await caches.delete(cache);
}
})
})()
)
})
This is the only code that worked for me.
It is my adaptation of Mozilla documentation :
//Delete all caches and keep only one
const cachNameToKeep = 'myCache';
//Deletion should only occur at the activate event
self.addEventListener('activate', event => {
var cacheKeeplist = [cacheName];
event.waitUntil(
caches.keys().then( keyList => {
return Promise.all(keyList.map( key => {
if (cacheKeeplist.indexOf(key) === -1) {
return caches.delete(key);
}
}));
})
.then(self.clients.claim())); //this line is important in some contexts
});

How to clear cache of service worker?

So, I have an HTML page with service worker,
the service worker cache the index.html and my JS files.
The problem is when I change the JS, the change doesn't show up directly on the client browser. Of course in chrome dev-tools, I can disable cache. But in chrome mobile, how do I do that?
I tried to access the site settings and hit the CLEAR % RESET button.
But it still loads the old page/load from cache.
I tried to use other browser or chrome incognito and it loads the new page.
Then, I try to clear my browsing data (just cache) and it works.
I guess that's not how it should work right? my user won't know if the page is updated without clearing the chrome browser cache.
If you know the cache name you can simply call caches.delete() from anywhere you like in the worker:
caches.delete(/*name*/);
And if you wanted to wipe all caches (and not wait for them, say this is a background task) you only need to add this:
caches.keys().then(function(names) {
for (let name of names)
caches.delete(name);
});
Use this to delete outdated caches:
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
// Return true if you want to remove this cache,
// but remember that caches are shared across
// the whole origin
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
});
Typically you update the CACHE_NAME in your service workers JS file so your worker installs again:
self.addEventListener('install', evt => {
evt.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(inputs))
)
})
Alternatively, to clear the cache for a PWA find the cache name:
self.caches.keys().then(keys => { keys.forEach(key => console.log(key)) })
then run the following to delete it:
self.caches.delete('my-site-cache')
Then refresh the page.
If you see any worker-related errors in the console after refreshing, you may also need to unregister the registered workers:
navigator.serviceWorker.getRegistrations()
.then(registrations => {
registrations.forEach(registration => {
registration.unregister()
})
})
The most elegant solution, with async/await:
const cacheName = 'v2';
self.addEventListener('activate', event => {
// Remove old caches
event.waitUntil(
(async () => {
const keys = await caches.keys();
return keys.map(async (cache) => {
if(cache !== cacheName) {
console.log('Service Worker: Removing old cache: '+cache);
return await caches.delete(cache);
}
})
})()
)
})
This is the only code that worked for me.
It is my adaptation of Mozilla documentation :
//Delete all caches and keep only one
const cachNameToKeep = 'myCache';
//Deletion should only occur at the activate event
self.addEventListener('activate', event => {
var cacheKeeplist = [cacheName];
event.waitUntil(
caches.keys().then( keyList => {
return Promise.all(keyList.map( key => {
if (cacheKeeplist.indexOf(key) === -1) {
return caches.delete(key);
}
}));
})
.then(self.clients.claim())); //this line is important in some contexts
});

Categories

Resources