Build a progressive web app using Vue CLI 3

Disclaimer March 2022

The project described in this project was created back in 2018. Technology progresses rapidly, which means that the exact implementation may no longer be valid. But I keep the blog here for now as the main ideas are still valid.


About a year ago I realized that modern web applications tend to behave like native apps. These apps are called progressive web apps (PWA) and with notifications, offline functionality and the ability to install on the homescreen, they really drive user engagement. Moreover, this meant being able to write my code in familiar programming languages and serve it on all devices: clearly a win-win situation. So I dived into the subject and recently launched my very first PWA for internal use in our company.

In this post I’ll describe my setup to create a very simple PWA. An app is qualified as a (baseline) PWA as it at least meets the following criteria

  1. Served over https
  2. Contains a web app manifest file
  3. Runs a service worker in the background

However, my goal is to extend this PWA with push notifications. Push notifications are the main drivers of user engagement and if used correctly, users spend more time in your app! For the sake of brevity though, in this post I’ll only discuss the Notifications API which is the first requirement for push notifications. In a follow up post I’ll elaborate more on the push notifications themselves.

In the remainder of this post I’ll explain how to create our simple PWA using the new Vue CLI 3 (@vue/cli) in combination with the PWA plugin created for @vue/cli. Under the hood, this plugin is based on Workbox. It took me a while to connect the dots here and I’ll try to shed some light on the simplest configuration.

Sample project

Let’s create a sample project with a simple button. Clicking the button should lead to a notification on your device (either desktop or mobile). That’s all, it’s the first step to create a PWA that utilizes push messages.

The code for this project can be found on Github.


As a start, I assume decent knowledge of Vue and ES6. Furthermore, you should be comfortable using the command line.

I am using the recently released @vue/cli which requires at least node 8.9 (I am running 8.11.2).

It is advised in the docs to install @vue/cli globally on your machine. After a successful installation you can check your version with

vue --version

I have 3.0.3 at the moment. Installation is described in the docs here.

Installing and configuring the project

Although the new @vue/cli has a graphical user interface option (just run ‘vue ui’), we mainly work from the command line to see how things work in the most simple way. Before running the commands below, make sure you navigate to the directory where you would like to start a new project.

vue create project-name

guides you through the installation. Choose ‘Manually select features’ and check at least the following options: Babel, PWA Support, Router, Vuex.

Of course you may add a linter, but for simplicity I don’t. I do however add a CSS Preprocessor because I like to write my CSS in LESS.

Furthermore, choose Y for history mode for Vue Router and ‘dedicated config files’ when asked where to place configs for the various modules we use. At the end you may choose to save this configuration. If saved, you can use it the next time you run the vue create command.

The project will be installed (this may take a while). Upon completion, cd into your project directory and start the dev server with the command

npm run serve

Note that the development and build commands changed compared to the previous version of the Vue CLI. ‘npm run serve’ replaces the old ‘npm run dev’. ‘npm run build’ is a replacement for the old ‘npm run prod’.

Loading our page for the first time

After executing npm run serve, open localhost:8080 in your browser and you should see something like

This default installation/app has two pages setup (Home and About) and uses Vue Router. Furthermore, the app makes use of two @vue-cli-plugins: babel and pwa. A few links to help you get started are provided as well.

We will change the homepage for the purpose of this post and we’ll create something like this

This is achieved by the following steps:

  1. Replace the logo in /src/assets by your own logo (keep the same filename though).
  2. Remove the import and all references to the HelloWorld component in /src/views/Home.vue
    Note that the project created by @vue/cli uses a slightly different folder structure compared to the previous version: components and page views are separated in their own directories for a better structure of the project.
  3. Add the button (including the @click directive) to the template.
  4. Add the click method for the button in the method section of the script part
  5. Add some styling to the button by adding a style tag.
    Since we use Less as preprocessor, we add the attribute lang=”less” to the style tag.

Adding PWA support

With the @vue/cli-plugin-pwa installed we have PWA support out of the box. However, out of the box this plugin mainly works as a black box. The default configuration used is not visible as no configuration file is supplied. However, the default configuration doesn’t support (push) notifications. Hence, we need to configure the plugin.

Although the docs of the @vue/cli are generally awesome, it took me quite some time of search and read to get the things working the way I wanted. Below I’ll sum up the steps to be taken.

  1. Start the configuration by adding a file named vue.config.js to the root of your project as described in the official docs.
  2. Add a key ‘pwa’ as described here. The docs direct us to the plugin documentation here.
  3. Now we have to choose our ‘workboxPluginMode’. There are two options, GenerateSW (default) and InjectManifest. Luckily, a link to the workbox docs is supplied which explains us which mode to choose in which situation. Since we like to have fine grained control over our service worker (i.e. extending the service worker with our own code to be able to use the Web Push API), we have to set the mode to InjectManifest.
  4. This mode requires us to set a second field, named workboxOptions and add the key swSrc. This key refers to the location of our own service worker. The plugin will use this file to create the final service worker during the production build. Just setting this field to ‘src/service-worker.js’ will do the trick.
  5. Don’t forget to actually create this file (keep it empty for now) in your src directory.
  6. (optional) Add a theme color.

Here is how the vue.config.js should look like now:

Quick note about WorkboxOptions

Although the docs tell us differently, the swSrc field is required to be named ‘service-worker.js’ as this is the filename that is expected by the src/registerServiceWorker.js file that will be created automatically by @vue/cli-plugin-pwa.

If you are running your app using the ‘npm run serve’ command and you are wondering why there is no service worker applied or created, that is due to the fact that ‘npm run serve’ runs in development mode and the service worker is only added in production mode, according to the if-block in src/registerServiceWorker.js. You’ll find more on environment variables and modes in the official docs. I tried to make it work in development mode to speed up development but to no avail. For now I just run the build step to test after I made my changes.

Building and running in production

So far we didn’t actually do something with the service worker, but this may be a good time to check if everything works correctly in production, i.e., is the service worker we created working as expected?

Therefore, we stop the npm run serve command (Ctrl + c) and we build for production with

npm run build

This creates a /dist folder that can be deployed to a server. Since this deployment process is different for each type of server, the docs have setup guides for various platforms. I recently deployed our own app on Laravel Forge and wrote a post on how to deploy. If you plan to deploy this code, make sure to add an SSL certificate to serve the app over https, otherwise the green button won’t show up.

However, deploying your app to a production server after each small change is tedious. Luckily it is possible to test it locally! For this we need an HTTP server as described by the docs, which is easy with the node package serve

Hence, I assume you are following along and you are able to execute

serve -s dist

and open localhost:5000 in your browser. The page with our logo is displayed with the button below. To check if everything is working as expected, open your developer console and look for the log message we wrote in our own service-worker.js file.

So what is the flow of things going on here?

When building the app for production, the @vue/cli-plugin-pwa builds up a service-worker.js file in the /dist folder based on our configuration in vue.config.js. The content of the manifest.json file is injected and our app shell, consisting of the index.html file and our compiled assets (images, css and js) is precached. In addition, the code in our /src/service-worker.js file is appended and this results in the file /dist/service-worker.js. Upon loading the page in the browser, this service worker is registered by the plugin.

Note on existing older service workers

If you don’t see the messages in the console it is possible that you already have an (old) active service worker running on the page. By default, these are not deactivated/ replaced because it may well be that the page breaks. To get rid of it, simply open your devtools tab ‘application’, click on the Clear Storage item in the left column and scroll down in the right column until you see the button ‘Clear site data’. Click that button, reload the page and you should be good to go.

Adding a notification

Next, we continue with the implementation of notifications. There are a lot of apps and websites nowadays that ask for permission to send notifications. Let’s add a simple notification to our app.

Note that we already added a button to our user interface to ‘enable notifications’. However, at the moment all it does is showing an alert once clicked. We’ll change that by following these steps:

  1. On click of the button we ask the user for permission to send notifications (once permission is granted, this automatically means we are granted to send push messages as well).
  2. Once granted, we create a notification and send it to the device of the user.

The code for the whole component is shown below. The askPermission method is executed on button click.

As you can see, I use a local variable to know if notifications are supported in the currently used browser. This variable is also used to hide the button if they are not supported. It checks for support for both service workers and the Notifications API.

Next, we ask for permission using the requestPermission method and based on the result (user either grants permission or not) we continue.

The code to show a notification (after permission is granted) is the method showNotification.

Note that we do check for the availability of the service worker again. Strictly speaking this is not necessary as we already checked before.

The way we create the notification may seem over complicated, but we do a little work in advance for future push messages. In principle it is possible to create the notification directly, without waiting for the navigator.serviceWorker.ready method. However, notifications based on push messages are send from the service worker (more about this in a future post). Therefore, we send this notification from the service worker also.

To be able to send messages from the service worker, we first have to retrieve the current service worker registration. This is done by the navigator.serviceWorker.ready which returns a promise. Once it resolves we obtain the active service worker registration from which we may call the showNotification method.

This method expects a title of the notification and an options object. The options we use in this example are quite simple, you can find more options in the docs

  • body => an extra description for below the title
  • icon => an icon to appear at the right of the notification (on android)
  • image => an image that is shown with the notification (on android)
  • vibrate => the way your smartphone vibrates (if enabled for notifications, this can be configured on android for every webapp individually) when the notification is received
  • badge => small image shown in the ‘android notification drawer’

A note on the badge: At first I just used a small version of our logo, which consists of a combination of the letters P and L in a circle with a green border:

However, your android OS creates the badge itself from the image you pass in as the ‘badge’ option. This resulted in a badge showing up as a white circle. Later on I created a badge containing only the P/L letter and this resulted in a nice badge that is recognizable by the user!

Note that if permission was granted and you click the button again, you won’t be  asked for permission again, but the notification will be send right away. The permission is stored in the browser. In chrome we may view our notification permissions here. I probably should have changed the text on the button after permission granted…

Notifications and Push notifications

Note that a notifications and a push notification are not the same. A notification uses the Notification API to display a message on the user’s device and is created on the client. A push notification is more complex and originates on the server, communicates with the browser vendor and finally makes its way to the service worker on the client. The client uses the Notification API to display it in the end.

Code on Github

The code for this example is available on Github, a demo for this specific post is no longer available as we proceeded with the follow up post.


In this post I described how to create a simple progressive web app with Vue extended with notifications, using the latest technology. The @vue/cli was used for quickly scaffolding the app and the @vue/cli-pwa-plugin was used to add PWA support. Note that the PWA depends on an active service worker so always build your app for production before testing the PWA functions such as notifications.

In the follow up post I will extend this app with push messages. Therefore, we need to create a backend API (e.g., in Laravel) and start to use webpush!

Mijn Twitter profiel Mijn Facebook profiel
Pim Hooghiemstra Webdeveloper and founder of PLint-sites. Loves to build complex webapplications using Vue and Laravel! All posts
View all posts by Pim Hooghiemstra

10 thoughts on “Build a progressive web app using Vue CLI 3

  1. Mike Loffland

    Thanks for the great explanation!

    When using ‘serve -s dist’ and navigating to the http://localhost:5000 url… everything works as expected. But, when using the network ip… the code does not. I am assuming there is some kind of browser lockdown on using service workers not being served out via localhost (or an https secured ip)?

    1. Pim Hooghiemstra Post author

      Hi Mike, thanks for your reply!

      I guess it has something to do with the service worker not being there. I know that localhost:5000 is typically seen as https, but that probably doesn’t work that way for the network IP. Haven’t really tried it myself though.

    1. Pim Hooghiemstra Post author

      Hi Ruben,

      Unfortunately, (mobile) Safari doesn’t support service workers at the moment. The button is only shown in browsers that support both the Notification API and service workers.

  2. Abu Zubair

    Hi thank you for great article and easy to follow,

    Should I always run npm run build every time i made a changes?

    1. Pim Hooghiemstra Post author

      Hello Abu,

      Generally, the service worker is only registered when in production mode. Hence, even when you are working locally, it only works with production builds.

      During development it is a bit tedious to wait for the build for production step everytime you make changes, but I am afraid that for the moment that is the way to go.

  3. NotifyVisitors

    I have been gone through several posts on this very subject but the satisfactory information that I found here is something that all other blogs are missing about progressive web app.

  4. John

    Thanks for this great and well-written blog post. I followed it to the letter. Please keep writing them.

    One tip you could add (which happened to me) is that some people may need to take localhost:5000 off the notifications-blocked list in Settings -> Advanced -> Site Settings -> Notifications. If not permission will be automatically ‘denied’.


Leave a Reply

Your email address will not be published. Required fields are marked *