Handling Service Worker updates in your Vue PWA
Tags: vue, pwa, webdev, javascript
Published: 2020-05-14 19:21:52

## Table Of Contents
  * [Updating the Service Worker registration](#sw-reg)
  * [Making an update mixin](#mixin)
  * [Updating our UI](#update-ui)
  * [Skipping Service Working waiting](#skip-waiting)
  * [Updating our Service Worker](#sw-update)
  * [Reloading the page](#reloading-page)
  * [TL;DR](#tl-dr)

Vue.js is awesome. It's easy to use, extremely flexible, and has some awesome DevTools. Since you're reading this I'll assume you already know this.

The Vue CLI is one such DevTool, allowing us to quickly and easily add plugins to our Vue App. Given the growing trend and popularity of building PWA's it comes as no surprise that the Vue CLI has its own PWA plugin and, for the most part, it's as awesome as you'd expect.

If all you're trying to do is add some basic PWA magic to your site the `plugin-pwa` _**is**_ pure magic. Just install it and out of the box you get your manifest for install-ability and a service worker for precaching. There's even a host of [configuration](https://cli.vuejs.org/core-plugins/pwa.html#configuration) options if you want to get fancy with a theme color, change your PWAs name, etc.

What it doesn't do out of the box is handle activating the service worker when an updated one is found. So let's add that ourselves.

# Updating the Service Worker registration<a name="sw-reg"></a>

When you install the `plugin-pwa` it adds a `registerServiceWorker.js` file to `src` with some basic config and events. For more on this file feel free to checkout [register-service-worker](https://www.npmjs.com/package/register-service-worker) on npm. The only part we need (for this tutorial) is the `update()` function. On a fresh install it looks like this:

```javascript
updated () {
  console.log('New content is available; please refresh.')
}
```

We'll need to modify this function a bit to get it to do more than just log to our console when there's an update.

First things first, we'll need access to the new service worker that was just registered. Luckily `register-service-worker` handles this for us. According to their documentation:

> The `ready`, `registered`, `cached`, `updatefound` and `updated` events passes a [ServiceWorkerRegistration](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration) instance in their arguments.

Perfect! Simply pass the `ServiceWorkerRegistration` in as an argument and we're off to the races. The next issue we'll face is getting those registration details to our Vue app. So, we can use a [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) to handle that. Now our `update()` function should look something like this:

```javascript
updated(registration) {
  console.log('New content is available; please refresh.')
  document.dispatchEvent(
    new CustomEvent('swUpdated', { detail: registration })
  )
}
```

We're now passing in our `ServiceWorkerRegistration` and triggering an event we can listen to called `swUpdated` and sending the `ServiceWorkerRegistration` as an event property.

# Making an update mixin<a name="mixin"></a>

Next up is listening for this event from within our Vue app. There are many places you can put this code, depending on your projects structure, but I opted to make it a mixin.  Just personal preference, you do you. Let's create a file in `src` called `mixins/update.js` and set it up to listen for our event and make a callback when it's triggered:

```javascript
export default {
  created() {
    document.addEventListener('swUpdated', this.updateAvailable, { once: true })
  },
  methods: {
    updateAvailable(event) {
      console.log(event)
    }
  }
}
```
_A note about the `once` option; setting this option to true allows the listener to be called only once **AND** removes the listener once invoked._

Let's store the SW registration so we can use it later in the update process. While we're at it we can add a flag to control showing our future 'Update available; please refresh.' message to our user. Should look something like this:

```javascript
export default {
  data() {
    return {
      registration: null,
      updateExists: false,
    }
  },
  created() {
    document.addEventListener('swUpdated', this.updateAvailable, { once: true })
  },
  methods: {
    updateAvailable(event) {
      this.registration = event.detail
      this.updateExists = true
    }
  }
}
```

# Updating our UI <a name="ui-update"></a>

One of the reasons why I used a mixin for this is so I can easily use this functionality anywhere I want in my app (App.vue, a layout, somewhere else) and with any UI kit I'm using on that project. I love [Vuetify](https://vuetifyjs.com/en/) so for the sake of this tutorial lets roll our 'Update' message to our user with that.

And for simplicity lets just throw in it our `App.vue` file. Again, you can do this wherever is right for your app. 

In your `App.vue` template add a snackbar component with a button that will allow the user to update the app when prompted.  Something like this:

```html
<v-snackbar bottom right :value="updateExists" :timeout="0" color="primary">
  An update is available
  <v-btn text @click="refreshApp">
    Update
  </v-btn>
</v-snackbar>
```

You'll also need to import the update mixin. Because we're adding the mixin we'll have access to all the data and functions of the mixin. 

# Skipping Service Working waiting <a name="skip-waiting"></a>

Let's pop back into our update mixin and create the `refreshApp` function. We'll use this function to reset the `updateExists` flag and force the new service worker to become the active one. Once a service worker is registered it "waits" until the perviously registered SW is no longer controlling the client. By telling the new SW to "skip waiting" we literally *skip* this waiting period.

Our `refreshApp` function will look a little something like this:

```javascript
refreshApp() {
  this.updateExists = false
  // Make sure we only send a 'skip waiting' message if the SW is waiting
  if (!this.registration || !this.registration.waiting) return
  // Send message to SW to skip the waiting and activate the new SW
  this.registration.waiting.postMessage({ type: 'SKIP_WAITING' })
}
```

# Updating our Service Worker <a name="sw-update"></a>

If you're using the default settings for `plugin-pwa` or you have `workboxPluginMode` set to `'GenerateSW'` you can skip this next part as the plugin automatically generates a service worker with the proper listener.  Otherwise you need to add the following listener to your service worker after your standard workbox config:

```javascript
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting()
  }
})
```

We're almost done.  Now we just need to reload the page once the new service worker is active so our changes can be seen.

# Reloading the page <a name="reloading-page"></a>

Back in our update mixin lets listen for the `controllerchange` event from our service worker.

In `created()` add:

```javascript
navigator.serviceWorker.addEventListener('controllerchange', () => {
  // We'll also need to add 'refreshing' to our data originally set to false.
  if (this.refreshing) return
  this.refreshing = true
  // Here the actual reload of the page occurs
  window.location.reload()
})
```

And that's it! Deploy this update and manually clear your apps storage. Then deploy another update, refresh the page, and you should see your popup:

<!-- Popup screenshot here -->

Clicking the update button should trigger the site to reload and you'll see your changes!

# TL;DR <a name="tl-dr"></a>

* Update serviceworker registration:

```javascript
// src/registerServiceWorker.js

// Standard SW registration script.
// Auto generated by the Vue CLI PWA Plugin
import { register } from 'register-service-worker'

if (process.env.NODE_ENV === 'production') {
  register(`${process.env.BASE_URL}service-worker.js`, {
    //...
    // When the SW is updated we will dispatch an event we can listen to in our .vue file
    updated(registration) {
      console.log('New content is available; please refresh.')
      document.dispatchEvent(
        new CustomEvent('swUpdated', { detail: registration })
      )
    },
    //...
  })
}
```

* Make an update mixin:

```javascript
// src/mixins/update.js

export default {
  data() {
    return {
      // refresh variables
      refreshing: false,
      registration: null,
      updateExists: false,
    }
  },

  created() {
    // Listen for our custom event from the SW registration
    document.addEventListener('swUpdated', this.updateAvailable, { once: true })

    // Prevent multiple refreshes
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      if (this.refreshing) return
      this.refreshing = true
      // Here the actual reload of the page occurs
      window.location.reload()
    })
  },

  methods: {
    // Store the SW registration so we can send it a message
    // We use `updateExists` to control whatever alert, toast, dialog, etc we want to use
    // To alert the user there is an update they need to refresh for
    updateAvailable(event) {
      this.registration = event.detail
      this.updateExists = true
    },

    // Called when the user accepts the update
    refreshApp() {
      this.updateExists = false
      // Make sure we only send a 'skip waiting' message if the SW is waiting
      if (!this.registration || !this.registration.waiting) return
      // send message to SW to skip the waiting and activate the new SW
      this.registration.waiting.postMessage({ type: 'SKIP_WAITING' })
    },
  },
}
```
* Update the UI (vuetify example):

```javascript
// src/App.vue

// I use Vuetify in almost all of my Vue apps so this is how __I__ handle alerting the user to an update.
// Your implementation may change based on your UI
<template>
  <!-- normal vue views stuff here -->
  <!-- ... -->
  <v-snackbar bottom right :value="updateExists" :timeout="0" color="primary">
    An update is available
    <v-btn text @click="refreshApp">
      Update
    </v-btn>
  </v-snackbar>
</template>

<script>
import update from './mixins/update'

export default {
  name: 'App',
  data: () => ({
    //
  }),
  mixins: [update],
  ...
}
</script>
```

* Update the Service Worker:

```javascript
// src/service-worker.js

// If you are using 'GenerateSW' (default) for your workboxPluginMode setting this file is auto generated for you.
// If you are using 'InjectManifest' then add this to your custom SW after your standard workbox config

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting()
  }
})
```

## Boom, Done.

So, what do you think? Anything about my implementation you would change? Do you handle SW updates differently? Hell, tell me if you just don't like my writing style. I won't get better or have the confidence to write more posts without your feedback!