I have a PWA built with Aurelia and compiled with Webpack, using the Workbox Plugin that generates the sw.js
service worker file. I'm trying to make the "New version available" user notification so that the user can activate the new version when clicking on a link within the app.
I am successfully downloading and installing the new version in the background, and even detecting that a new version is ready. However, when I try to call the skipWaiting()
method to force refresh of the page with the new version, it fails, because apparently I don't have the right scope or object.
The main problem is probably that I can't edit the actual sw.js because it is automatically generated. The examples all suggest the use of self.skipWaiting();
, but I don't know how to access that object.
webpack.config.js
new WorkboxPlugin({
globDirectory: './dist',
globPatterns: ['**/*.{html,js,css,woff,woff2,ttf,svg,eot,jpg}'],
swDest: './dist/sw.js',
clientsClaim: true,
skipWaiting: false, // because I want to notify the user and wait for response
}),
index.ejs
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => {
// make the registration available globally, for access within app
window.myServiceWorkerReg = reg;
// Check for update on loading the app (is this necessary?)
return reg.update();
})
.catch(console.error);
}
</script>
app.js
activate() {
// listener for service worker update
this.swReg = window.myServiceWorkerReg;
console.warn('[app.js] ACTIVATE.', this.swReg);
this.swReg.addEventListener('updatefound', () => {
// updated service worker found in reg.installing!
console.warn('[app.js] UPDATE FOUND.', this.swReg);
const newWorker = this.swReg.installing;
newWorker.addEventListener('statechange', () => {
// has the service worker state changed?
console.warn('[app.js] STATE HAS CHANGED.', newWorker, newWorker.state);
if (newWorker.state === 'installed') {
// New service worker ready.
// Notify user; callback for user request to load new app
myUserMessage({ clickToActivate: () => {
// reload fresh copy (do not cache)
console.warn('[app.js] Post Action: skipWaiting.');
// this.swReg.postMessage({ action: 'skipWaiting' });
// THIS IS THE LINE THAT FAILS
this.swReg.skipWaiting();
}});
}
});
});
}
Everything works fine except the last line (this.swReg.skipWaiting();
). Has anyone else used webpack+workbox plugin and gotten the skipWaiting to happen as a result of user interaction?
I finally got it to work. One problem was that I was using an older version of workbox-webpack-plugin. The current version (4.2) includes a listener in the service worker that can trigger self.skipWaiting()
when a message is posted to the worker like this:
newWorker.postMessage({ type: 'SKIP_WAITING' });
But you have to ensure that the config has skipWaiting: false;
and that you are using the latest version.
These instructions are pretty good:
https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin
However, I tweaked things to work well between my App and the service worker instantiation in the index.ejs file.
webpack.config.js
new GenerateSW({
globPatterns: ['dist/**/*.{html,js,css,woff,woff2,ttf,svg,eot,jpg}'],
swDest: 'sw.js',
clientsClaim: true,
skipWaiting: false,
})),
index.ejs
<script>
if ('serviceWorker' in navigator) {
// register the service worker
navigator.serviceWorker.register('/sw.js')
.then(reg => {
window.myWorkerReg = reg;
// Check for update on loading the app (is this necessary?)
return reg.update();
})
.catch(console.error);
// The event listener that is fired when the service worker updates
navigator.serviceWorker.addEventListener('controllerchange', function () {
// when the service worker controller is changed, reload the page
if (window.swRefreshing) return;
window.location.reload();
window.swRefreshing = true;
});
}
</script>
app.js
activate() {
// listener for service worker update
this.swReg = window.myWorkerReg;
if (this.swReg) {
// if there is already a new service worker ready to install, prompt user
if (this.swReg.waiting) {
this.promptUpdateServiceWorker(this.swReg.waiting);
}
// add listener to detect when a new service worker is downloaded
this.swReg.addEventListener('updatefound', () => {
// updated service worker is being installed
const newWorker = this.swReg.installing;
// add listener to detect when installation is finished
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
// New service worker ready to activate; prompt user
this.promptUpdateServiceWorker(newWorker);
}
});
});
}
}
// listener for buildVersion
buildVersionChanged(buildVersion) {
// through proprietary code, we've detected a new version could be downloaded now
window.myWorkerReg.update();
}
// New service worker ready. Show the notification
promptUpdateServiceWorker(newWorker) {
// actual code for UI prompt will vary; this is pseudocode
uiPrompt('New_version_ready').then((response) => {
if (response.approved) {
// reload fresh copy (do not cache)
newWorker.postMessage({ type: 'SKIP_WAITING' });
}
});
}