Building a PWA with Vue.js from scratch

This tutorial will walk through the basics of how to create a Progressive Web Application using Vue.js and the Vue CLI. The full project is available on GitHub.

Note: I won’t be explaining any basic Vue.js concepts in depth, as this tutorial assumes you’re already somewhat familiar with the framework.

Let’s jump right in. If you don’t have it already, install Vue CLI first:

~/code$ yarn global add @vue/cli

Next, let’s create a new project:

~/code$ vue create my-pwa-app

Vue CLI defaults to Babel and Linter/Formatter (eslint) support only, but for this project we will need PWA support as well. Choose Manually select features from the next menu and select Progressive Web Application (PWA) Support as well.

Answer the other questions by hitting Enter and sit back while Vue CLI scaffolds out your initial project.

If you already have a Vue project created, you can simply add PWA support using vue add pwa instead.

Once it completes, open up the project folder using your preferred text editor and let’s take a look.

As you can see, Vue CLI has created a handful of files and folders. Let’s break down some important ones.

  • /public is where all of our static files will go. Anything placed in this directory will be available as-is without undergoing any transformation or optimization by the build process.
  • /public/index.html is what the browser will look for when it loads
  • /src is where all of the actual code for getting stuff done happens. During the build process, these files get packaged up and optimized.
  • /src/main.js is the entrypoint for the app. It takes care of initializing Vue, specifying the base template (App.vue), and registering the service worker.
  • Not yet shown is the /dist folder, which is (re)created each time you build your project. When it gets time to actually deploy our application in production, we’re going to want to point our web root to this folder.

Fire up a live server and open up http://localhost:8080/ in your browser to see what it looks like. From within your project folder:

~/code/my-pwa-app$ yarn serve

In addition to displaying a large Vue logo, it also displays content generated by the HelloWorld component in /src/components. Great!

Let’s open up Chrome Developer tools and click over to the Audits tab. If we run an audit, Lighthouse will show that we need to do a little bit more.

One thing to note is that we’re not viewing a production build. Since we’re using yarn serve, assets are compiled on the fly and the browser window is updated each time we save a change to the code. This is also the reason why our Performance score is so dismally low, and why the Progressive Web App category is grayed out. Let’s switch over to a production build and see if anything changes. Stop the live server with Ctrl+C and let’s build our project for real.

~/code/my-pwa-app$ yarn build
~/code/my-pwa-app$ serve -s dist

You’ll notice serve defaults to using port 5000, so let’s visit the new address in the browser and re-run the audit:

That’s a bit better. As you can see, the PWA portion is no longer grayed out. Congratulations, you’ve just built your first Progressive Web Application. 👏

But we’ve only scratched the surface. Let’s look at what actually makes a PWA.

First, you need a manifest.json. Like other static assets, this JSON file normally lives in /public and specifies the name of the app, theme colors, icons, and a litany of other app-specific things. However since we used Vue CLI to scaffold our project, it is currently being (re)generated each time we build. To specify these values, we’ll need to create a vue.config.js (which we’ll get to later).

Secondly, your PWA needs a service worker. Service workers function as the backbone for the app. They can check for updates, manage cached data, and almost anything else your app needs to do in the background. To use a service worker, it has to be registered, which Vue CLI has already taken care of. Let’s look at /src/registerServiceWorker.js:

/* eslint-disable no-console */

import { register } from 'register-service-worker'

if (process.env.NODE_ENV === 'production') {
  register(`${process.env.BASE_URL}service-worker.js`, {
    ready () {
        'App is being served from cache by a service worker.\n' +
        'For more details, visit'
    registered () {
      console.log('Service worker has been registered.')
    cached () {
      console.log('Content has been cached for offline use.')
    updatefound () {
      console.log('New content is downloading.')
    updated () {
      console.log('New content is available; please refresh.')
    offline () {
      console.log('No internet connection found. App is running in offline mode.')
    error (error) {
      console.error('Error during service worker registration:', error)

As you can see, this script makes use of the register-service-worker package, which performs the entire SW registration process and emits a handful of events which we can listen for and handle here. Let’s head back to our production build in the browser and check out the console messages that our service worker is already generating.

Right off the bat, we can see two lines that indicate our service worker is doing something, corresponding to the ready() and registered() events.

Success! In this lesson, we’ve learned how to use Vue CLI to scaffold out a basic Progressive Web Application, explored the various files and folders it creates, and learned how to run a production build process.

But you might be thinking, what’s so special about a PWA? Well, as the name implies, it’s an app. And apps can be installed so that they live on the device and not inside the browser.

We’ll tackle this step in the next article, “Building a PWA with Vue.js from scratch (part 2)”.