PWAs (Progressive Web Applications) has already been identified as the future of web applications and the reason is quite obvious. PWAs let you build web apps that are capable of delivering native app-like experiences to users.
From sending push notifications, to caching data for offline retrieval, to background sync, Progressive web apps have got you completely covered. PWAs can also ensure that users are engaged and up to date with dynamic data even with very poor network connections.
Progressive Web App (PWA) is a term used to denote web applications that use the latest web technologies. Progressive Web Apps, also known as Installable Web Apps or Hybrid Web Apps, are regular web pages or websites but can appear to the user like traditional applications or native mobile applications. The application type attempts to combine features offered by most modern browsers with the benefits of mobile experience. - Wikipedia
This article demonstrates how to build a simple realtime PWA with Vue.js and Pusher. Vue.js is a Progressive Web Framework for JavaScript, it’s easy to use, and requires relatively little code to produce awesome results.
For the realtime part of this application, we will be plugging in Pusher’s JavaScript library. Pusher is a realtime engine that makes it easy to add realtime functionalities to applications.
In this article, we will be building a cryptocurrency application called “KryptoWatcher”. Its function is to display the price updates of three cryptocurrencies (Bitcoin, Ethereum, and Litecoin) in realtime. The price updates will be obtained from the Cryptocompare API.
KryptoWatcher will also be able to travel five days into the past and retrieve coin data for those days. Here’s a visual display of what the final application will look like:
The best part of it all is that, after this app runs once, it can run again and display coin data even without an internet connection. This is possible because we’ll build KryptoWatcher
to cache the coin data on the first run.
Let’s start putting the pieces together.
To follow along in this tutorial, you will need to have the following:
Once you have requirements we can move on to setting up our application.
Create a Pusher account, if you have not already, and then set up your application as seen in the screenshot below.
When you have completed the set up, take note of your Pusher application keys as we will need them later on.
You can think of the Vue CLI tool as a lightweight tool for scaffolding Vue.js projects. To start building our application we will use the Vue CLI tool to pull in the Vue PWA template that we will be working with.
To create our application run the following command on your terminal:
$ vue init pwa krypto-watcher
You’ll be presented with prompts and a few ‘Yes’ or ‘No’ questions. You can answer most as you see fit, however, for the “Y” or “N” prompts, since we do not require the additional functionalities and features, let’s respond with “N” to all the queries.
The template gives us awesome PWA features out of the box. One such feature is the service worker. The service worker allows our application to work offline.
💡 A service worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don't need a web page or user interaction.
To install the dependencies, go to your terminal window and run the following command:
$ cd krypto-watcher && npm install
If you take a look at your project directory, you will find that it contains a few subfolders: build
, config
, src
, static
, test
. Let’s open the build/webpack.prod.conf.js
file and take a quick peek at the SWPrecacheWebpackPlugin
object:
1new SWPrecacheWebpackPlugin({ 2 cacheId: 'krypto-watcher', 3 filename: 'service-worker.js', 4 staticFileGlobs: ['dist/**/*.{js,html,css}'], 5 minify: true, 6 stripPrefix: 'dist/' 7 })
What this does is generate a new service worker when the application is built (with the npm run build
command).
The service worker will cache all the files that match the glob expression, for offline access, in staticFileGlobs
which currently points to a non-existent dist
folder. The dist
directory will be created when we build our application.
Let’s start building out our application component by component.
Similar to other modern JavaScript libraries and frameworks like React, Vue allows us to create components when building applications. Components help us keep our application modular and ensure that apps can be separated into reusable modules.
Let’s build KryptoWatcher
by creating three reusable components:
Intro
component which will hold the introductory markup and styles for the application.Current
component which will display coin prices in realtime.Previous
component which will display coins prices from ‘x days ago’.Let us start creating the components. We will be doing them manually however you can always use an NPM package like this to make it easier to create components. Create a src/components
directory and create the following files in the directory: Intro.vue
, Current.vue
, and Previous.vue
.
This component has no special functionalities as it just holds the intro markup and styles that will make the app presentable. The HTML goes between the template
tags and the styles go in the styles
tag.
In the Intro.vue
file paste the following:
1<template> 2 <header class="hero"> 3 <div class="bar logo"> 4 <h3>KryptoWatcher</h3> 5 <span class="monitor"><span class="monitorText">receive updates</span></span> 6 </div> 7 <h1>Realtime PWA that displays updates on cryptocurrencies</h1> 8 <h2>Bitcoin, Ethereum, Litecoin?</h2> 9 </header> 10 </template> 11 <script>export default { name: 'app' }</script> 12 13 <style scoped> 14 header { 15 background: linear-gradient(to bottom right, rgb(0, 193, 131),rgb(50, 72, 95)); 16 padding: 1em; 17 margin-bottom: 1em; 18 text-align: center; 19 height: 300px; 20 color: #fff; 21 } 22 header h3 { 23 color: white; 24 font-weight: bold; 25 text-transform: uppercase; 26 float: left; 27 } 28 bar { padding: 20px; height: 48px; } 29 .monitor{ 30 text-transform: uppercase; 31 float:right; 32 background-color: rgba(255, 255, 255, 0.2); 33 line-height: 23px; 34 border-radius: 25px; 35 width: 175px; 36 height: 48px; 37 margin: auto; 38 } 39 .monitor:hover, monitorText:hover { cursor:pointer; } 40 .monitorText{ 41 width: 104px; 42 height: 23px; 43 font-weight: bold; 44 line-height: 50px; 45 font-size: 14px; 46 } 47 header h1 { padding-top: 80px; width: 80%; margin: auto; } 48 header h2{ padding-top:20px; } 49 </style>
That is all for the intro component.
In the Current.vue
component, we’ll write some HTML that displays the prices in realtime as they are updated. Open the file and paste the following inside the file:
1<template> 2 <div> 3 <h2>Current prices of coins</h2> 4 <div id="btc" class="currency"> 5 <label>1 BTC</label> 6 <p>${{currentCurrency.BTC}}</p> 7 </div> 8 <div id="eth"class="currency"> 9 <label>1 ETH</label> 10 <p>${{currentCurrency.ETH}}</p> 11 </div> 12 <div id="ltc"class="currency"> 13 <label>1 LTC</label> 14 <p>${{currentCurrency.LTC}}</p> 15 </div> 16 </div> 17 </template>
Below the template
tags, we will have the script
tag. This will be where we will handle the scripting of the component. Below the template
tag in the same file, paste the following code:
1<script> 2 export default { 3 name: 'app', 4 props: { 5 currentCurrency: { type: Object } 6 }, 7 data () { 8 return {} 9 } 10 } 11 </script>
The script above specifies the props
the Current
component should expect. It will be getting it, currentCurrency
, from the parent component App.vue
.
Lastly, below the script
tag, let’s include the style
for the component. Paste the following code after the script
tag:
1<style scoped> 2 .currency { 3 border: 1px solid #F5CE00; 4 border-radius: 15px; 5 padding: 2em 0em; 6 display: inline-block; 7 width: 30%; 8 } 9 div p { font-size: 2rem; } 10 h2 { font-size: 1.5em; } 11 </style>
That’s all for the Current
component.
This component should display the prices of coins in the past, five days at most. We’ll also display the dates of each of the days.
Inside the Previous.vue
file paste the following code:
1<template> 2 <div> 3 <h2>Previous prices of coins</h2> 4 <div id="first"> 5 <h2>Date: {{previousCurrency.yesterday.DATE}}</h2> 6 <p><label>1 BTC:</label> {{previousCurrency.yesterday.BTC}}</p> 7 <p><label>1 ETH:</label> {{previousCurrency.yesterday.ETH}}</p> 8 <p><label>1 LTC:</label> {{previousCurrency.yesterday.LTC}}</p> 9 </div> 10 <div id="second"> 11 <h2>Date: {{previousCurrency.twoDays.DATE}}</h2> 12 <p><label>1 BTC:</label> {{previousCurrency.twoDays.BTC}}</p> 13 <p><label>1 ETH:</label> {{previousCurrency.twoDays.ETH}}</p> 14 <p><label>1 LTC:</label> {{previousCurrency.twoDays.LTC}}</p> 15 </div> 16 <div id="third"> 17 <h2>Date: {{previousCurrency.threeDays.DATE}}</h2> 18 <p><label>1 BTC:</label> {{previousCurrency.threeDays.BTC}}</p> 19 <p><label>1 ETH:</label> {{previousCurrency.threeDays.ETH}}</p> 20 <p><label>1 LTC:</label> {{previousCurrency.threeDays.LTC}}</p> 21 </div> 22 <div id="fourth"> 23 <h2>Date: {{previousCurrency.fourDays.DATE}}</h2> 24 <p><label>1 BTC:</label> {{previousCurrency.fourDays.BTC}}</p> 25 <p><label>1 ETH:</label> {{previousCurrency.fourDays.ETH}}</p> 26 <p><label>1 LTC:</label> {{previousCurrency.fourDays.LTC}}</p> 27 </div> 28 <div id="fifth"> 29 <h2>Date: {{previousCurrency.fiveDays.DATE}}</h2> 30 <p><label>1 BTC:</label> {{previousCurrency.fiveDays.BTC}}</p> 31 <p><label>1 ETH:</label> {{previousCurrency.fiveDays.ETH}}</p> 32 <p><label>1 LTC:</label> {{previousCurrency.fiveDays.LTC}}</p> 33 </div> 34 </div> 35 </template>
In the script
section, we’ll be receiving the previousCurrency
object from the parent component, App.vue
. In the same file paste the following code after the template
tag:
1<script> 2 export default { 3 name: 'app', 4 props: { 5 previousCurrency: { type: Object } 6 }, 7 data () { 8 return {} 9 } 10 } 11 </script>
Lastly, some styles to help things stay pretty:
1<style scoped> 2 #first, #second, #third, #fourth, #fifth { 3 border: 1px solid #F5CE00; 4 padding: 2em 0em; 5 max-width: 90%; 6 margin: 3px auto; 7 } 8 #first p, #second p, #third p, #fourth p, #fifth p { 9 display: inline-block; 10 padding: 0em 1.5em; 11 font-size: 1.5rem; 12 } 13 h2 { font-size: 1.5em; } 14 </style>
That’s pretty much all the business we have with the three components, they are pretty straightforward. Most of the complexity and app logic are buried in the root component, App.vue
. Let’s explore that next.
The root component is included by default in every fresh Vue installation in the src/App.vue
file, so we don’t need to create it. Unlike the other components we created earlier, the root component holds the logic and is more complex than them.
We’ll keep the template
tag of the root component simple. We include the earlier components, Intro.vue
, Current.vue
, and Previous.vue
, as custom tags and pass in the appropriate props
.
In the App.vue
file, replace the contents with the following:
1<template> 2 <div> 3 <intro></intro> 4 <div id="body"> 5 <div id="current"> 6 <current v-bind:currentCurrency="currentCurrency"></current> 7 </div> 8 <div id="previous"> 9 <previous v-bind:previousCurrency="previousCurrency"></previous> 10 </div> 11 </div> 12 </div> 13 </template>
Next, let’s add some script
and start adding logic in the script
section. Paste the following below the template
tag:
1<script> 2 import Intro from './components/Intro.vue'; 3 import Current from './components/Current.vue'; 4 import Previous from './components/Previous.vue'; 5 6 export default { 7 name: 'app', 8 components: {Intro, Current, Previous}, 9 data() { 10 return { 11 currentCurrency: {BTC: '', ETH: '', LTC: ''}, 12 previousCurrency: { 13 yesterday: {}, twoDays: {}, threeDays: {}, fourDays: {}, fiveDays: {} 14 } 15 } 16 }, 17 methods: { 18 // Stub 19 }, 20 created() { 21 // Stub 22 } 23 } 24 </script>
The script above does not do much but it sets the stage for our logic. We have set all the defaults for the data
we will be using in the application and we have defined the created
method that is called automatically during Vue’s component lifecycle. We also imported the components we will be using in the application.
Before we start adding script logic, let’s add some style for the root component. Below the script
tag, paste the following code:
1<style> 2 @import url('https://fonts.googleapis.com/css?family=Lato'); 3 * { 4 margin : 0px; 5 padding : 0px; 6 font-family: 'Lato', sans-serif; 7 } 8 body { height: 100vh; width: 100%; } 9 .row { display: flex; flex-wrap: wrap; } 10 h1 { font-size: 48px; } 11 a { color: #FFFFFF; text-decoration: none; } 12 a:hover { color: #FFFFFF; } 13 a:visited { color: #000000; } 14 .button { 15 margin: auto; 16 width: 200px; 17 height: 60px; 18 border: 2px solid #E36F55; 19 box-sizing: border-box; 20 border-radius: 30px; 21 } 22 #body { 23 max-width: 90%; 24 margin: 0 auto; 25 padding: 1.5em; 26 text-align: center; 27 color:rgb(0, 193, 131); 28 } 29 #current { padding: 2em 0em; } 30 #previous { padding: 2em 0em; } 31 </style>
We need to populate the method
object with actual methods. We’ll start by defining the methods that will retrieve coin prices for previous days.
Since we are getting data from a remote API, we need an HTTP client to pull in the data for us. In this article, we’ll be using the promise based HTTP client axios to make our HTTP requests. We also need moment to easily work with dates.
To add Axios and Moment.js to our project, run the following command in your terminal:
npm install --save vue-axios axios vue-momentjs moment
💡
vue-axios
andvue-momentjs
are Vue wrappers around the Axios and Moment.js packages.
When the installation is complete, we will globally import the packages to our application. Open the src/main.js
file and in there replace:
import App from './App'
with:
1import App from './App' 2 import moment from 'moment'; 3 import VueMomentJS from 'vue-momentjs'; 4 import axios from 'axios' 5 import VueAxios from 'vue-axios' 6 7 Vue.use(VueAxios, axios) 8 Vue.use(VueMomentJS, moment);
Next, we want to go back to our root component and build out the methods
object. In the methods
object, let’s create the first method. Paste the following code inside the methods
object in the App.vue
file:
1_fetchDataFor: (key, daysAgo) => { 2 var date = this.$moment().subtract(daysAgo, 'days').unix() 3 let fetch = (curr, date) => this.axios.get(`https://min-api.cryptocompare.com/data/pricehistorical?fsym=${curr}&tsyms=USD&ts=${date}`) 4 5 this.axios 6 .all([fetch('BTC', date), fetch('ETH', date), fetch('LTC', date)]) 7 .then(this.axios.spread((BTC, ETH, LTC) => { 8 this.previousCurrency[key] = { 9 BTC: BTC.data.BTC.USD, 10 LTC: LTC.data.LTC.USD, 11 ETH: ETH.data.ETH.USD, 12 DATE: this.$moment.unix(date).format("MMMM Do YYYY"), 13 } 14 15 localStorage.setItem(`${key}Prices`, JSON.stringify(this.previousCurrency[key])); 16 })) 17 },
The method above is a helper method for fetching the coin exchange rate within a specified period and saving the response in localStorage
and the this.previousCurrency
object. We will use this later in the code.
Next, paste the following function inside the methods
object alongside the one we added above:
1_fetchDataForToday: () => { 2 let url = 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH,LTC&tsyms=USD' 3 4 this.axios.get(url).then(res => { 5 localStorage.setItem('BTC', this.currentCurrency.BTC = res.data.BTC.USD), 6 localStorage.setItem('ETH', this.currentCurrency.ETH = res.data.ETH.USD), 7 localStorage.setItem('LTC', this.currentCurrency.LTC = res.data.LTC.USD) 8 }) 9 },
The method above simply fetches the coin data for the current date and saves the response to localStorage
and the this.currentCurrency
object.
Next, inside the created()
method of the root component, paste in the following code:
1if ( ! navigator.onLine) { 2 this.currentCurrency = { 3 BTC: localStorage.getItem('BTC'), 4 ETH: localStorage.getItem('ETH'), 5 LTC: localStorage.getItem('LTC'), 6 } 7 8 this.previousCurrency = { 9 yesterday: JSON.parse(localStorage.getItem('yesterdayPrices')), 10 twoDays: JSON.parse(localStorage.getItem('twoDaysPrices')), 11 threeDays: JSON.parse(localStorage.getItem('threeDaysPrices')), 12 fourDays: JSON.parse(localStorage.getItem('fourDaysPrices')), 13 fiveDays: JSON.parse(localStorage.getItem('fiveDaysPrices')) 14 } 15 } else { 16 this._fetchDataFor('yesterday', 1) 17 this._fetchDataFor('twoDays', 2) 18 this._fetchDataFor('threeDays', 3) 19 this._fetchDataFor('fourDays', 4) 20 this._fetchDataFor('fiveDays', 5) 21 this._fetchDataForToday() 22 }
In the code above, we have defined the code to fetch the current currency from localStorage
if the client is offline. If the client is online though, it fetches the data from the API.
Everything should be working now except the realtime functionality.
Now that we have a functional application, we would like to add some realtime functionality so we see updates as they happen.
We will be using Pusher to provide this functionality, if you haven’t, create your Pusher application from the Pusher dashboard as you will need the: app_id
, key
, secret
and cluster
.
We need a backend server to trigger events to Pusher, we will be using Node.js to build the backend for this article.
To get started, create a new file in the root directory of our application and call it server.js
.
In this server.js
file, we’ll be using Express
as the web framework so we need to pull that in. We’ll also pull in axios
, Pusher
and body-parser
since we’d be making references to them in our code.
In your terminal type in the following command:
$ npm install --save express axios body-parser pusher
When the installation is complete, open the server.js
file and in the file paste in the following code:
1const express = require('express'); 2 const path = require('path'); 3 const bodyParser = require('body-parser'); 4 const app = express(); 5 const Pusher = require('pusher'); 6 const axios = require('axios'); 7 8 9 // Initialise Pusher 10 var pusher = new Pusher({ 11 appId: 'PUSHER_APP_ID', 12 key: 'PUSHER_APP_KEY', 13 secret: 'PUSHER_APP_SECRET', 14 cluster: 'PUSHER_APP_CLUSTER', 15 encrypted: true 16 }); 17 18 // Body parser middleware 19 app.use(bodyParser.json()); 20 app.use(bodyParser.urlencoded({ extended: false })); 21 22 // CORS middleware 23 app.use((req, res, next) => { 24 res.setHeader('Access-Control-Allow-Origin', '*') 25 res.setHeader('Access-Control-Allow-Credentials', true) 26 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 27 res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type') 28 next() 29 }); 30 31 // Routes 32 app.get('/', _ => res.send('Welcome')); 33 34 // Simulated Cron 35 setInterval(_ => { 36 let url = 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH,LTC&tsyms=USD'; 37 38 axios.get(url).then(res => { 39 pusher.trigger('price-updates', 'coin-updates', {coin: res.data}) 40 }) 41 }, 5000) 42 43 // Start app 44 app.listen(8000, () => console.log('App running on port 8000!'));
💡 You need to replace
PUSHER_APP_ID
,PUSHER_APP_KEY
,PUSHER_APP_SECRET
, andPUSHER_APP_CLUSTER
with the details from your Pusher application dashboard.
In the Express app above, we import our dependencies and then instantiate Pusher. We then register some middleware including the CORS middleware so we don’t get cross origin request errors.
Next, we have a “Simulated Cron” that runs after 5 seconds. The job is to fetch the updates from the server and send the updates to Pusher. Our Vue application can then subscribe to the Pusher channel, pull the changes and display them.
Finally, we tell the Node app to listen on port 8000. To start the Node server, run the command below:
$ node server.js
This will start a Node server and the simulated cron will start running and sending events to Pusher.
To access our API server from the Vue application, we can create a proxy in config/index.js
and run the dev server and the API backend side-by-side. All requests to /api
in our frontend code will be proxied to the backend server.
Open the config/index.js
and make the following modifications:
1// config/index.js 2 module.exports = { 3 // ... 4 dev: { 5 // ... 6 proxyTable: { 7 '/api': { 8 target: 'http://localhost:8000', 9 changeOrigin: true, 10 pathRewrite: { 11 '^/api': '' 12 } 13 } 14 }, 15 // ... 16 } 17 }
In the proxyTable
we attempt to proxy requests from /api
to localhost:8000
.
To use Pusher
on the client side of our application we need to pull in the pusher-js
. Run the following command in your terminal:
$ npm install --save pusher-js
When the installation is complete, we will import pusher-js
to the root component. Within the script
tag add the following at the top:
import Pusher from 'pusher-js'
Next we will initialize Pusher with the app credentials from the Pusher dashboard and subscribe to a channel in the created()
life cycle hook. Open the App.vue
and add this to the bottom of the created()
method in the else
block:
1let pusher = new Pusher('PUSHER_APP_KEY', { 2 cluster: 'PUSHER_APP_CLUSTER', 3 encrypted: true 4 }); 5 6 let channel = pusher.subscribe('price-updates'); 7 8 channel.bind('coin-updates', data => { 9 this.currentCurrency = { 10 BTC: data.coin.BTC.USD, 11 ETH: data.coin.ETH.USD, 12 LTC: data.coin.LTC.USD 13 } 14 });
In the code above, we subscribe to receive updates on the price-updates
channel. Then we bind to the coin-updates
event on the channel. When the event is triggered, we get the data and update the currentCurrency
.
That’s all now. You can build the application by running the command below:
$ npm run dev
This should start and open the Vue PWA on your browser. To make sure you receive updates, make sure your Node server is running.
As it is, the application already functions but is not a PWA in true sense of the term. So let us work on making the application a PWA with offline storage. The build process already automatically generates the service worker when the application is built so let’s build the application. Run the following command to build the application:
$ npm run build
This command creates a dist
folder in our working directory and also registers a new service worker. Let’s serve this dist
directory and take a peek at the generated service worker in a Chrome web browser.
We’ll serve this application using an NPM package called Serve. Run the following command to install it:
$ npm i serve -g
When the installation is complete, we will use the package to serve the application. Run the following command to serve the application:
$ serve dist
We should get an output that looks like this:
If we navigate to this address http://localhost:5000
on our web browser, we’d see our application as it was the last time, no obvious changes except for the fact that the app is now a PWA.
We can inspect this feature by opening the browser’s dev tools and clicking on the “Application” tab. Here’s what we should see:
Our app registered a service worker that caches the app shell on the first run, thanks to the Vue PWA template.
💡 An application shell (or app shell) refers to the local resources that your web app needs to load the skeleton of your user interface (UI). Think of your app's shell like the bundle of code you would publish to a native app store when building a native app.
In this tutorial, we have seen how to write a simple realtime PWA with Vue.js, Pusher and Service Workers. We also saw how to cache dynamic values from a remote API using the Web Storage API’s storage object. There is a lot more you can do with PWAs and Vue, but this is a good introduction so far.