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. Probably the easiest way is just going here, 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:
nvm install latest
For some particular version you can execute
nvm install 15.4.0
Then, remember to use
it!
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 which helps with setting up the project. Install it with:
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:
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:
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
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, 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:
- How to run service worker in development server
- How to control cache behavior
- 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 in your project:
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:
// 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
):
// 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:
// src/service-worker.js
console.log("Hello world from our SW!");
and run the dev server
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 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).
// 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
and on subsequent something like that
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:
- If you don't care about that logging coming from
registerServiceWorker.js
, just don't import it insrc/main.js
and problem will be gone. - 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:
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:
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.