Vue 3, PWA & service worker
Tags: vue, pwa, javascript
Published: 2021-01-03 17:07:45

Recently I started playing with Vue. Of course, starting with "hello world" calculator with well-documented Vue 2 was not an option, so I decided to go with simple PWA in Vue 3. Setting up a project wasn't as easy as it may appear, so I'll describe it here for anyone interested (and future reference for myself).

I'll describe everything from (nearly) scratch, so hopefully it'll be of use for complete beginners. I **will not** explain philosophy of Vue, PWA or service workers - it will be just about setting those things up.

I'm using Win10, so I'll describe the process from this PoV (however, it matters only for Node installation).

## Node, npm and Vue
As with all JS projects, it's simpler to do it with Node & npm.

If you don't already have them, I recommend installing Node with [nvm](https://github.com/coreybutler/nvm-windows). Probably the easiest way is just going [here](https://github.com/coreybutler/nvm-windows/releases), downloading latest *nvm-setup.zip*, extracting and running the installer. After that, you should be able to use `nvm` in your command prompt. If you want to install latest **stable** version just go with:
```bash
nvm install latest
```
For some particular version you can execute
```bash
nvm install 15.4.0
```
Then, remember to `use` it!
```bash
nvm use 15.4.0
```
With Node, npm should be automatically installed as well. For me, Node version is 15.4.0 and npm is 7.3.0.

To make our lives easier, there's also [Vue CLI](https://cli.vuejs.org/guide/) which helps with setting up the project. Install it with:
```bash
npm install -g @vue/cli
```
It will allow you to use `vue` command from your terminal. For me, `vue --version` returns `@vue/cli 4.5.9`. 

Now, we can start with our mini-project.

## Creating project
Creating a new project with Vue CLI is extremely simple. Just go with:
```bash
vue create our-app-name
```
Then using arrows just select the options. I picked:
```Manually select features```
and then selected with spacebar `Progressive Web App (PWA) support`. Press Enter to continue, then choose Vue version to `3.x`, `ESLint with error prevention only`, `Lint on save`, `In dedicated config files`, type `n` and press Enter to generate the project (it will take 1-2 minutes).

*Of course you can choose different options. Only PWA support is necessary*

## Running it
Generated project is runnable out of the box. First of all, remember to navigate to the created project folder, then run development server:
```bash
cd our-app-name
npm run serve
```
Output should give you addresses where you can access your generated app. For me it's `http://localhost:8080/` (if you want to stop it, just `CTRL+C` it)

Note that currently service worker is not working - if you go to *Application > Service worker* in DevTools you won't see it. Generated project makes service worker active only in production build. Let's check it.

To create a production build, run
```bash
npm run build
```
Give it some time and it will create `dist` directory in your project folder. Now you need to host it somewhere. I'd recommend [Web Server for Chrome](https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb?hl=en), as it's very easy to use and works fine (I tried also Python simple http server, but it didn't work correctly for me, so watch out for that). Just select your *dist* folder in the server and run it. At `http://127.0.0.1:8000` you should be able to access your site. Now you can find information about service worker in the *Application* tab of DevTools and see some console logs about it.

## Taming service worker
That's great! Everything works! So what's the problem? Problem appears, when you want to control caching with service worker by yourself and check it during development without constantly creating production builds.

I'll show 3 things now:
1. How to run service worker in development server
2. How to control cache behavior
3. How to use external modules in production build

### SW in dev server
*Quick warning - SW is disabled in development by default, because it may cache some newly edited scripts/assets and you won't be able to see your changes. Keep that in mind and disable SW in dev if you don't need it to avoid "Why it doesn't change?!" problems.*

*Another warning - it's probably not the best, most optimal way to do that... but it's simple and works :)*

**Case: we want to have service worker active in development mode and be able to control its caching policy.**

Not diving into details, let's make it happen.

First of all, you need to install [serviceworkerW-webpack-plugin](https://www.npmjs.com/package/serviceworker-webpack-plugin) in your project:
```bash
 npm install -D serviceworker-webpack-plugin
```
Then in root of your project (next to `src` folder) add new file `vue.config.js` with that content:
```javascript
// vue.config.js

const path = require("path");
const ServiceWorkerWebpackPlugin = require("serviceworker-webpack-plugin");

module.exports = {
  configureWebpack: {
    plugins: [
      new ServiceWorkerWebpackPlugin({
        entry: path.join(__dirname, "./src/service-worker.js")
      })
    ]
  }
};
```
and modify `src/main.js` to include those lines (before `createApp`):
```javascript
// src/main.js

// other imports...
import runtime from "serviceworker-webpack-plugin/lib/runtime";

if ("serviceWorker" in navigator) {
  runtime.register();
}

// createApp...
```
Finally, add `service-worker.js` in `src` with some "Hello world" content:
```javascript
// src/service-worker.js

console.log("Hello world from our SW!");
```
and run the dev server
```bash
npm run serve
```
When you navigate to your app in a browser you should see the message from service-worker in the console. Success!

### Controlling caching - Workbox
Writing SW from scratch may be interesting... but let's make it simple and use [Workbox](https://developers.google.com/web/tools/workbox) for that. It's already installed, so you just need to import it in the SW script. I'm not going to explain everything from the below snippet, because it's done very clearly on *Getting started* page of Workbox. It's just example of setting specific rules for data matching some RegEx (images in that case).
```javascript
// src/service-worker.js

import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
import { Plugin } from 'workbox-expiration';
import { precacheAndRoute } from 'workbox-precaching';

precacheAndRoute(self.serviceWorkerOption.assets);

registerRoute(
    /\.(?:png|gif|jpg|jpeg|svg)$/,
    new StaleWhileRevalidate({
        cacheName: 'images',
        plugins: [
            new Plugin({
                maxEntries: 60,
                maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
            }),
        ],
    })
);

```
Just brief comment about that `precacheAndRoute` line - `self.serviceWorkerOption.assets` comes from that `serviceworker-webpack-plugin` we installed before and contains all the statics assets in our app. 

Now, even if you are in development mode, you should see some workbox logs in the console. On the first page load it will be
![wb1](https://dev-to-uploads.s3.amazonaws.com/i/egs8exk7sh2kowip9lq0.png)
and on subsequent something like that
![wb2](https://dev-to-uploads.s3.amazonaws.com/i/c49jm3mhxugknlac4912.png)
If you go to *Network* tab in DevTools and simulate offline mode, the app should still load properly. 

Great! Two problems solved - we have granular control over our service worker and it works in development mode.

#### Fixing SW on prod
In the meantime, unfortunately we messed up the production version of the app. If you run `npm run build` and look at it, it may seem fine at the beginning, but it's not. First of all, on subsequent refreshes you can see 
```New content is available; please refresh.```
log all the time, although you don't change anything. Also, if you check *Application* tab, you will see two service workers all the time - one active, the second one waiting to activate. Even if you force the update, there will be another waiting after the refresh.

Issue comes from double registration - one SW is registered in `main.js`, the second one comes from generated `registerServiceWorker.js`. That's the problem I wasn't able to overcome in good way, but I came up with two acceptable solutions:
1. If you don't care about that logging coming from `registerServiceWorker.js`, just don't import it in `src/main.js` and problem will be gone. 
2. If you want to keep those console logs, but are fine with having SW working **only** on prod (but keep the control of caching rules) and with a bit more complex way of importing modules in SW, it requires a bit more effort:
Firstly, change `vue.config.js` contents to:
```javascript
module.exports = {
  pwa: {
    workboxPluginMode: "InjectManifest",
    workboxOptions: {
      swSrc: "src/service-worker.js"
    }
  }
};
```
then revert changes you made in `src/main.js` (i.e. remove anything related to `serviceworker-webpack-plugin`). Finally, you need to change `src/service-worker.js` to **NOT** use `import` and use precaching with different argument. If you want to use some external modules, use `importScripts` with CDN link (momentjs below for example; usage is stupid but demonstrates way of doing that). Note how workbox names are expanded now:
```javascript
importScripts('https://momentjs.com/downloads/moment.min.js');

workbox.precaching.precacheAndRoute(self.__precacheManifest);

const cacheExpTime = moment().add(1, 'day');
const cacheTimeLeft = moment.duration(cacheExpTime.diff(moment())).asSeconds();

workbox.routing.registerRoute(
    /\.(?:png|ico|gif|jpg|jpeg|svg)$/,
    new workbox.strategies.StaleWhileRevalidate({
        cacheName: 'images',
        plugins: [
            new workbox.expiration.Plugin({
                maxEntries: 60,
                maxAgeSeconds: cacheTimeLeft, // 1 day
            }),
        ],
    })
);

``` 


Surely there's some 3rd option, which lets you keep logging and everything else, but I don't know, how to configure Webpack properly. If you have any simple solution, I'll be more than happy to read about it in the comments :)

## Conclusion
If you want very simple caching technique and keeping handling only static assets by service worker is fine, the generated project is definitely enough. However, if you want more control over your service worker to cache ex. API calls, you need to tweak it somehow. I hope that above tips, how to do that and how to handle the development mode will be useful.

As said, it's definitely not the best and only solution. It's just a starter option for some Vue newbies (like me) to deal with service workers in a reasonable way.