I have implemented Workbox to generate my service worker using webpack. This works – I can confirm revision is updated in the generated service worker when running "generate-sw": "workbox inject:manifest"
.
The problem is – I have noticed my clients are not updating the cache after a new release. Even days after updating the service worker my clients are still caching the old code and new code will only load after several refreshes and/or unregistering the service worker. For each release I have confirmed that the revision is updated.
I understand that I need to implement skipWaiting
to ensure the clients gets updated – especially PWA. I have read, and tried to follow the 3rd approach here: https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68.
My app mounts in app.js
I have added this code to
serviceWorker-base.js
addEventListener('message', function(messageEvent){ if (messageEvent.data === 'skipWaiting') return skipWaiting(); });
I have this code in app.js
const runServiceWorker = true const serviceWorkerAvailable = ('serviceWorker' in navigator) ? true : false // reload once when the new Service Worker starts activating let refreshing navigator.serviceWorker.addEventListener('controllerchange', function() { if (refreshing) return refreshing = true window.location.reload() } ) function promptUserToRefresh(reg) { // this is just an example - don't use window.confirm in real life; it's terrible if (window.confirm("New version available! OK to refresh?")) { reg.waiting.postMessage('skipWaiting') } } function listenForWaitingServiceWorker(reg, callback) { console.log('listenForWaitingServiceWorker') function awaitStateChange() { reg.installing.addEventListener('statechange', function() { if (this.state === 'installed') callback(reg) }) } if (!reg) return if (reg.waiting) return callback(reg) if (reg.installing) awaitStateChange() reg.addEventListener('updatefound', awaitStateChange) } // Register service worker if (runServiceWorker && serviceWorkerAvailable) { navigator.serviceWorker.register('/serviceWorker.js') .then( (registration) => { console.log('Service worker registered', registration) listenForWaitingServiceWorker(registration, promptUserToRefresh) // <-- Added to existing code }) }else{ console.log('Service worker disabled - process.env.NODE_ENV', process.env.NODE_ENV) }
The problem with this code is that promptUserToRefresh()
only gets called on initial service worker install, not when a new service worker is waiting!
Also, I get the below error when accepting the first install.
TypeError: registration.waiting is null promptUserToRefresh app.js:154 awaitStateChange app.js:162
The error gets triggered in promptUserToRefresh(registration)
by
registration.waiting.postMessage('skipWaiting')
I have also tested this approach with the same result: https://github.com/GoogleChrome/workbox/issues/1120
Advertisement
Answer
The code now works after simply re-arranging it!
Updated app.js
// *** PWA Functionality START *** // 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 }) }else{ console.log('Service worker disabled - process.env.NODE_ENV', process.env.NODE_ENV) } // 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') }) // *** PWA Functionality END *
Maybe the below (removed) lines caused all the trouble?
// reload once when the new Service Worker starts activating let refreshing navigator.serviceWorker.addEventListener('controllerchange', function() { if (refreshing) return refreshing = true window.location.reload() } )
All that remains now is figuring out how not to show the prompt on first visit to the app / install! (^__^)/