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
- Served over https
- Contains a web app manifest file
- 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.
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.
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
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:
- Replace the logo in /src/assets by your own logo (keep the same filename though).
- 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.
- Add the button (including the @click directive) to the template.
- Add the click method for the button in the method section of the script part
- 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.
- Start the configuration by adding a file named vue.config.js to the root of your project as described in the official docs.
- Add a key ‘pwa’ as described here. The docs direct us to the plugin documentation here.
- 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.
- 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.
- Don’t forget to actually create this file (keep it empty for now) in your src directory.
- (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:
- 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).
- 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.
Demo and code
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 a future 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!
Matthew pointed out that the current app only scores 69 in the Lighthouse audit. There are two main reasons being:
- No 200 response in offline mode (we’ll tackle this in a follow up post)
- Issue with the start_url in the manifest.json file
The latter is easy to fix: just replace the current value of start_url (‘/index.html’) with ‘/’.
However, auditing again still led to a score of 69. Investigating this a bit further (by trying to add the app to my homescreen on the desktop using the devTools), I found that although the start_url issue has been resolved, we still encounter issues due to the fact that offline mode is not implemented yet.
Stay tuned for a follow up post where I’ll dive into this.
Originally published at www.blog.plint-sites.nl on October 19, 2018.