Getting Started With PWAs - A Practical Introduction

Introduction

 
There are many ways to present software to a user, such as a Website, mobile application, desktop application, etc.
 
Progressive Web Appplications (PWA) are a method of presenting / distributing software to users. A PWA allows the user to launch your website or web app from their home screen as if it were a native application. PWAs also employ powerful caching mechanisms to allow for offline usage of your web app, and to make your app load much faster. If you're new to progressive web apps, I recommend reading What is PWA (Progressive Web Apps).
 
But how can we allow our users to install our website/web app? 
 
There are only a few components we need in order to convert our website into a PWA,
  • A Service Worker
  • An app manifest
  • An app icon
Note
It is also necessary for your website, service workers, and manifest to be served by a web server. If your server is not running on localhost, then you must have an https URL. http is fine for localhost, however.
 

The Web App

 
For this article, I will be using Node.js, Express.js, HTML, and (vanilla) JavaScript to create this web application.
 

Create the Node Server

 
For this project I will create a Node.js project with a main file called index.js and install express by opening a terminal, navigating to the project folder and typing:
  1. npm install express  
Once express is installed, we can edit index.js and type this code:
  1. var express = require('express');  
  2. var app = express();  
  3. var path = require('path');  
  4.   
  5. app.use(express.static(__dirname + '/public'));  
  6.   
  7. app.get('/'function(req, res) {  
  8.     res.sendFile(path.join(__dirname + '/public/index.html'));  
  9. });  
  10.   
  11. app.listen(8080, () => console.log('listening...'));  
Now we have a web server to host our website. We will be serving our content from the /public/ directory within our project folder. From this folder we will serve a static file index.html which will be the homepage for our site.
 
It is important to note the following line of code,
  1. app.use(express.static(__dirname + '/public'));    
This line is very important because we will be serving the content of our root directory to our client as static content. This will be necessary to load our manifest, service workers, and other static files we'll need for our PWA.
 

Create the homepage

 
Our website will be very simple. Just a basic "Hello World" in HTML that we can serve from our node/express server. 
 
Create a file in the /public/ directory called index.html and type this markup,
  1. <!DOCTYPE html>    
  2. <html lang="en">    
  3. <head>    
  4.     <meta charset="UTF-8">    
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">    
  6.     <title>Document</title>    
  7. </head>    
  8. <body>    
  9.      <p>Hello World!</p>  
  10. </body>    
  11. </html>     
Test to see if your web server is working correctly before moving forward.
 
Next, we will add code to our webpage to load the service worker.
 

What's a service worker? 

 
From developers.google.com: "A service worker is a type of web worker. It's essentially a JavaScript file that runs separately from the main browser thread, intercepting network requests, caching or retrieving resources from the cache, and delivering push messages."
 
The way I see it, service workers help us bridge the gap between a website and a locally-installed native application.
 

Load the Service Worker Into Our Page 

 
In our index.html file, we can add a script at the top of the page to load our service worker script. 
 
Note
In this article I am including the script on the HTML page itself for brevity, but you can also do this in a separate .js file.
 
In the <head> tag of our index.html we can add this code,
  1. <script>  
  2.         if('serviceWorker' in navigator){  
  3.             window.addEventListener('load', () => {  
  4.                 navigator.serviceWorker.register('serviceWorker.js')  
  5.                     .then(reg => {  
  6.                         console.log('Registered!', reg);  
  7.                     })  
  8.                     .catch(err => {  
  9.                         console.log('Registration failed:', err);  
  10.                     });  
  11.             });  
  12.         }  
  13.         else {  
  14.             console.log('Service workers are not supported by this browser.');  
  15.         }  
  16. </script>  
Let's go through this.
 
First, we are checking to see if 'serviceWorker' is present in the navigator object. This check will ensure that the browser actually supports service workers. If there is no support, we will instead log a warning and just ignore PWA functionality.
 
If service workers are supported, we begin registering our service worker script.
 
Note
This script doesn't exist yet. We will need to create it ourselves. 
 
Once the service worker script is registered, the promise returns the registration object. For this example we are simply logging it to the console.
 

Create the Service Worker

 
In the /public/ directory, create a file called serviceWorker.js
 
In this script, we will need several elements.
 
First, we can define which static files our PWA will precache to the user device. This list of files will download to the user device from the server before loading the page, which will allow for instantaneous load times, as well as offline support.
 
We can define our precache by typing this code at the top of our script,
  1. const cacheName = 'cache-v1';  
  2. const precache = [  
  3.     '/',  
  4.     'index.html',  
  5. ];  
To start, we only have index.html being served to the client. If you have any scripts or .css files, you will want to add them to that array.
 
Next, we will define several events for our service worker,
  1. The 'install' event, which will fire when the user installs our PWA
  2. The 'fetch' event, which will intercept any AJAX calls from the client side, before they reach the server

The Install Event

 
After the precache is defined, add this code to serviceWorker.js:
  1. self.addEventListener('install', event => {  
  2.     console.log('Install event fired');  
  3.     event.waitUntil(  
  4.         caches.open(cacheName)  
  5.             .then(cache => {  
  6.                 return cache.addAll(precache);  
  7.             })  
  8.         );  
  9. });   
To summarize this code, we are adding a listener for the 'install' event which, when fired, will add the files from our precache list into the local user cache.
 
Note
In a service worker script, self refers to the service worker object.
 

The Fetch Event

 
After the install event is registered, we can add this following code for our fetch event,
  1. self.addEventListener('fetch', event => {  
  2.     console.log('Request intercepted: ', event.request.url);  
  3.     event.respondWith(  
  4.         caches.match(event.request)  
  5.         .then(cached => {  
  6.             return cached || fetch(event.request);  
  7.         })  
  8.     );  
  9. });  
This will allow the service worker to intercept a client-side HTTP request, and return a locally-cached copy of the response if it's available, otherwise use fetch() to obtain the response from the server.
 
There are other events we can listen to in our service workers, but for the basics, this will suffice.
 

Add the Manifest

 
Arguably this is the most important step, because the manifest is what will tell the browser "Yes, this website is PWA-compatible"
 
Note
Not all browsers support loading an app manifest. Here is a table that shows the current browser support for manifest files.
 
Just like the service worker, our manifest will exist inside the /public/ directory of our project structure.
 
Create a file called manifest.json and add this configuration,
  1. {  
  2.     "name""MyPWA",  
  3.     "short_name""PWA",  
  4.     "icons": [{  
  5.         "src""icon.png",  
  6.         "sizes""192x192",  
  7.         "type""image/png"  
  8.     }],  
  9.     "start_url""/index.html",  
  10.     "display""standalone",  
  11.     "orientation""portrait",  
  12.     "background_color""#e17f27",  
  13.     "theme_color""#e17f27"  
  14. }  
This manifest file is where we can define the characteristics of our PWA, such as,
  • the app icon (and size variations) that will appear on the user's home screen
  • the background color of the splash screen when the app is loading
  • the theme color of the app
  • Whether or not to show the URL bar and "back" button ("display": "standalone" means we will not display the navigation)
For this example I'm using the following icon. Feel free to download and use it,
 
 
Make sure to place this icon into the /public/ directory where all the other static content lives.
 
Note
App icons must be at least 192x192
 

Final Steps

 
The only thing left to do is to add an "Install" button on our page to let the users know they can install our website as an app on their device!
 
For this example I will keep this very simple and show an "Install" button at the top of the page to prompt the user to install the PWA.
 
At the top of index.html's <body> tag, add this markup,
  1. <div id="addPwaPrompt" style="display:none;">  
  2.        <p>Add this application to your home screen!</p>  
  3.        <button id="btnAddPwa">Install</button>  
  4. </div>  
And after the closing </body> tag, we can add a script to handle triggering the install prompt,
  1. <script>  
  2.         let deferredPrompt;  
  3.         window.addEventListener('beforeinstallprompt', (e) => {  
  4.             e.preventDefault();  
  5.             deferredPrompt = e;  
  6.             addPwaPrompt.style.display = 'block';  
  7.         });  
  8.         document.getElementById('btnAddPwa').addEventListener('click', (e) => {  
  9.             deferredPrompt.prompt();  
  10.             deferredPrompt.userChoice.then((choiceResult) => {  
  11.                 if(choiceResult.outcome === 'accepted'){  
  12.                     console.log('user accepted prompt.');  
  13.                 }  
  14.                 deferredPrompt = null;  
  15.             });  
  16.         });  
  17. </script>  
We are using a deferred prompt mechanism here to avoid automatically firing the prompt when the page loads, in certain browsers.
 
The 'beforeinstallprompt' event on the window object fires if the browser detects PWA capabilities and the user has not installed the application to their device. Therefore, in this page, the install button and description text will be visible if the user has not installed the PWA.
 
Then, we register a click event handler on the install button that will prompt the user to install the app to their device using deferredprompt.prompt()
 
Once the prompt is shown, if the user clicks "Install," then the app icon will be added to their homepage (or desktop on a PC) and then the website can be loaded as a standalone application!
 

Conclusion

 
We created a simple "Hello World" website that is served via a simple Node/Express web server, and by just adding a few simple elements,
  1. Service Worker
  2. Web app manifest
  3. App icon
We were able to create an installable version of a website that can now be used online or offline, and can be started from the user's home screen just like a native app.
 
There is definitely much more to cover when it comes to PWAs, but hopefully this was a helpful introduction for you to get started converting your website today!
 
Be safe, and happy coding!