Give Users Control Over App Updates in Vue CLI 3 PWAs

A common UX pattern that isn’t so common out-of-the-box

Doug Allrich
5 min readDec 15, 2018

🔔 Now updated for Vue CLI 4! (2019–11–24)

Creating a fully functional Progressive Web App (PWA) using Vue CLI 3 and its associated PWA plugin is a pretty straightforward process. Not only can you get PWA projects up and running quickly, but because they are powered by the underlying workbox-webpack-plugin, many service worker configuration options are also available.

Unfortunately, due to the way service workers behave by default when updated, PWAs built using Vue CLI 3 do not provide a method for users to control when to apply an app update, nor do they notify users that one is even available.

Dan Fabulich’s exceptional article, and the Workbox team’s own Advanced Recipes page, both describe how to properly give users control over PWA app updates. Because the Vue CLI 3 PWA implementation is structurally different from the code examples given, however, several additional steps are needed if we want to provide our users with access to this nifty UX pattern.

The Files Involved

Assuming you have already created a functioning Vue PWA, we need to either create or modify the contents of these four project files:

.
├── src/
│ ├── App.vue
│ ├── registerServiceWorker.js
│ └── sw.js
└── vue.config.js

These are the only files that need to be touched to implement this feature. There is also an assumption that if you are modifying rather than creating one of these files, you are able to determine the proper placement of the suggested code changes.

The Vue Configuration File

If your project doesn’t yet have one, add vue.config.js to your project’s root folder, else modify your file to include this content in its pwa section:

vue.config.js

By default, Vue’s PWA plugin uses Workbox’s GenerateSW plugin mode to generate a complete service worker each time your project is deployed. To implement this feature, though, we need more control over our service worker code, which requires we instead make use of the InjectManifest plugin mode.

The swSrc property tells webpack where the seed service worker file containing our customized code can be found. The swDest property determines where to place the build version of our service worker and what to name it.

The Seed Service Worker File

The ins and outs of service workers are beyond the scope of this article, and your own service worker may include additional code. At a minimum, however, our sw.js seed service worker needs to include a precaching code section (so that it works with our PWA at all) and a message MessageEvent listener section.

By listening for a message event, our service worker will know if the user has decided to accept an app update invitation.

sw.js

Later, when you build for production, Workbox will inject some importScripts() into the seed service worker and place the resultant build version in the dist folder alongside your other built files.

The Service Worker Registration Helper File

Vue PWA projects include aregisterServiceWorker.js file. It abstracts out the service worker registration process and contains a list of events that are fired at different stages in the service worker’s lifecycle. Additionally, several of the events pass a ServiceWorkerRegistration instance in their arguments (e.g. updated(registration)), which Vue leaves out by default, but which are needed to implement this feature.

We need to modify this file’s contents in two places, the updated event and the registered event.

First, the updated event announces that a new service worker is available and waiting to be activated.

updated (registration) {
document.dispatchEvent(
new CustomEvent('swUpdated', { detail: registration })
);
}

We create and then dispatch a CustomEvent here because we want to be notified in App.vue if this event has fired. We also want to pass some custom data with our custom event using the provideddetail property. The data we’re passing to App.vue is our ServiceWorkerRegistration instance mentioned above, which we’ve named registration.

I’ve named my custom event swUpdated.

Second, we can optionally modify the registered event so that our service worker will automatically check for app updates on a routine basis.

registered (registration) {
setInterval(() => {
registration.update();
}, 1000 * 60 * 60); // e.g. hourly checks
}

The registered event is fired each time a service worker is activated so it’s a good place to insert our registration.update() code. Please note that we are passing a registration to our event as an argument.

If you prefer a full drop-in replacement registerServiceWorker.js file for your project instead of modifying your own, you can find an example here.

Hooking Up the App Component File

It’s time now to connect the dots in App.vue and complete our feature. Let’s break it down:

The markup in our example app is quite simple. It’s just a button that’s conditionally displayed and which is set to listen for its click event:

<template>
<button v-if="updateExists" @click="refreshApp">
New version available! Click to update
</button>
</template>

The initial component state section is similarly simple:

data() {
return {
refreshing: false,
registration: null,
updateExists: false,
};
}

In order to be informed that our service worker’s updated event occurred, we need to listen for our custom swUpdated event. A natural place to do that is inside our App component’s created lifecycle hook. This is also a good place to listen for our service worker’s controllerchange event so that the app will refresh once the updated service worker takes control of the page:

created () {
document.addEventListener(
'swUpdated', this.showRefreshUI, { once: true }
);
if (navigator.serviceWorker) {
navigator.serviceWorker.addEventListener(
'controllerchange', () => {
if (this.refreshing) return;
this.refreshing = true;
window.location.reload();
}
);
}
},

When our new service worker is installed (but waiting to be activated), the listener calls our showRefreshUI method, which stores our passed-in registration and displays our ‘New version available’ button:

showRefreshUI (e) {
this.registration = e.detail;
this.updateExists = true;
},

If the user clicks on the update button, our refreshApp method is called. Its main purpose is to trigger the message event listener inside our service worker file by passing it a skipWaiting message:

refreshApp () {
this.updateExists = false;
if (!this.registration || !this.registration.waiting) { return; }
this.registration.waiting.postMessage('skipWaiting');
},

If you prefer a full drop-in replacement App.vue file for your project instead of modifying your own, you can find an example here.

Alternative Option Using Vuetify

Using a component framework like Vuetify.js enables devs to more easily create attractive layouts using a variety of provided components.

For those who may prefer a more complete visual solution out-of-the-box, an alternative App.vue file is available here that accomplishes the same thing as described in the section above, but with a bit more functionality and improved appearance via their snackbar component. Just be sure you have Vuetify installed in your project for it to work.

Vuetify.js snackbar

There is also a GitHub repo available here that packages all of these files into one project (using the Vuetify version ofApp.vue) and functions as described in this article. It may be of use to you for a sample deploy or for a troubleshooting need.

That’s It!

Your users should now see an app update notification whenever one is available. They should also be able to choose whether to update the app by invoking skipWaiting() on their own terms, or to just keep using the current version until their next visit to the page.

--

--