Installable Web Apps: A Practical Introduction To PWAs with JavaScript and Node.js

June 26, 2018
Written by
Peter Mbanugo
Contributor
Opinions expressed by Twilio contributors are their own

jSGB3rZFrN9w0lA-ylxFwKQEjT3CvBcgavmwnBzxGUOpSoW8XcirVgWLNLQu0OeCOIIgO-5tssS7zyfo5_294KaS4Vm3q9pPw4fS41SKCCDumLM2Z6IcdkW32ZtbFtyoMc2-mTt

Progressive web applications (PWAs) can be built to make inconsistent internet connections much easier for users to handle. For example, offline-first web apps like this one that store shopping items locally. That example PWA was built with Hoodie and added a service worker script that enabled the app to load when it was offline. We can add more functionality to that PWA to make it even better for users.

In this post, we’re going to clone our progressive web app and make it installable. Being installable means it’ll be added to the device’s home screen and launched like a native app. To make it installable, we’re going to add a web app manifest and add Workbox to the build step to automatically generate a service worker.

Getting ready to code

To code along with me, you’ll need the following:

  1. NodeJS version 6.6.0 (or higher)
  2. npm version 3.6.3 (or higher)
  3. Source code from the previous post 

If you downloaded the source code, install the dependencies by running npm install in the command line.The app already has a service worker enabled and uses the Cache API to store some assets for the web app.

A service worker is a programmable network proxy that runs on a separate browser thread and allows you to intercept network requests and process them as you so choose.

The service worker file at public/sw.js already has the following content:

//file -> public/sw.js

const CACHE_NAME = "cache-v1";
const assetToCache = [
  "/index.html",
  "/",
  "/history.html",
  "/manifest.json",
  "/resources/mdl/material.indigo-pink.min.css",
  "/resources/mdl/material.min.js",
  "/resources/mdl/MaterialIcons-Regular.woff2",
  "/resources/mdl/material-icons.css",
  "/css/style.css",
  "/resources/dialog-polyfill/dialog-polyfill.js",
  "/resources/dialog-polyfill/dialog-polyfill.css",
  "/resources/system.js",
  "/js/transpiled/index.js",
  "/js/transpiled/history.js",
  "/js/transpiled/shared.js",
  "/hoodie/client.js"
];
self.addEventListener("install", function(event) {
  event.waitUntil(
    caches
      .open(CACHE_NAME)
      .then(function(cache) {
        return cache.addAll(assetToCache);
      })
      .catch(console.error)
  );
});
self.addEventListener("fetch", function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      if (response) {
        return response; 
      }
      return fetch(event.request);
    })
  );
});

In this code, when we have new assets or modify existing ones, we have to change the value of CACHE_NAME to invalidate the old cache and install the service worker script with updated resources. This process isn’t efficient. Instead, let’s look at how to generate a worker script through some configuration and automatically update the cache if assets changed. We’re going to use Workbox for that.

Generate a service worker script using Workbox

Precached resources are resources stored before they’re actually required. Our existing service worker precaches resources, but when something is changed it deletes the old cache and re-downloads the entire resources again. Workbox makes it easy to automatically generate a service worker that only downloads the changed resource and leaves the rest of the cache untouched. This is a smarter way of invalidating resources makes our app run faster and saves bandwidth.

Install Workbox

Because our build process is simple, we’re going to allow Workbox to generate the entire service worker script for us. Run the following command to install workbox-cli:

npm install -D workbox-cli@2.1.2

Add workbox-cli to the build process

Add a new file in the root directory of the project named workbox-cli-config.js. This file will  be used automatically by the Workbox cli to generate the final service worker. Add the below content to this file:

module.exports = {
  globDirectory: "public/",
  globPatterns: ["**/*.{css,ico,html,png,js,json,woff2}"],
  swDest: "./public/sw.js",
  globIgnores: ["icons/*", "js/src/*", "sw.old.js"],
  skipWaiting: true,
  clientsClaim: true,
  templatedUrls: {
    "/hoodie/client.js": "../.hoodie/cleint.js"
  }
};

Let’s get into more detail on a few of the parameters:

  • globDirectory tells it which directory it should pick files from and watch for changes
  • globPatterns tells it what kind of files to precache. The wildcard pattern we used tells it to cache every file with the specified extensions in globDirectory, or any of its subdirectories
  • swDest is where it outputs the script it generates
  • skipWaiting tells the service worker installed to activate as soon as it enters the waiting phase. If we hadn’t chosen to use Workbox, we would add self.skipWaiting(); in the function that handles the install event
  • clientsClaim instructs the latest service worker to take control of all clients as soon as it’s activated
  • templatedUrls is used for URLs generated based on some server-side logic. The file served when you request /hoodie/client.js can be found in your project at .hoodie/client.js. If this file changes, when we run our build process and refresh the app, the service worker will update that cache for this entry.

Now update package.json to call workbox as the last step in your build process by adding && workbox generate:sw to the “build” line of package.json:

"build": "babel public/js/src --out-dir public/js/transpiled && workbox generate:sw"

Build this by running this command in the command line:

npm run build

This will update the existing sw.js file. You should get something similar to this:

importScripts('workbox-sw.prod.v2.1.2.js');
/**
 * DO NOT EDIT THE FILE MANIFEST ENTRY
 *
 * The method precache() does the following:
 * 1. Cache URLs in the manifest to a local cache.
 * 2. When a network request is made for any of these URLs the response
 *    will ALWAYS comes from the cache, NEVER the network.
 * 3. When the service worker changes ONLY assets with a revision change are
 *    updated, old cache entries are left as is.
 *
 * By changing the file manifest manually, your users may end up not receiving
 * new versions of files because the revision hasn't changed.
 *
 * Please use workbox-build or some other tool / approach to generate the file
 * manifest which accounts for changes to local files and update the revision
 * accordingly.
 */
const fileManifest = [
  {
    "url": "css/style.css",
    "revision": "99559afa2b600e50f33cebcb12bd35e6"
  },
  {
    "url": "favicon.ico",
    "revision": "2ec6120d215494c24e7c808d0d5abf56"
  },
  {
    "url": "history.html",
    "revision": "240e2a52b8580117383162e8ec15fc00"
  },
  {
    "url": "index.html",
    "revision": "4a215dad3782fb0715224df00149cee9"
  },
  {
    "url": "js/transpiled/history.js",
    "revision": "f5d6af7aff37147b0c82043fe3153828"
  },
  {
    "url": "js/transpiled/index.js",
    "revision": "3b5384eca25ad783829434ee190ecb58"
  },
  {
    "url": "js/transpiled/shared.js",
    "revision": "38039d6e28ad31c85c4adc0c4bab2dc9"
  },
  {
    "url": "manifest.json",
    "revision": "cfada03439f24ccdb59dae8d4f6370d1"
  },
  {
    "url": "resources/dialog-polyfill/dialog-polyfill.css",
    "revision": "24599b960cd01b8e5dd86eb5114a1bcb"
  },
  {
    "url": "resources/dialog-polyfill/dialog-polyfill.js",
    "revision": "a581e4aa2ea7ea0afd4b96833d2e527d"
  },
  {
    "url": "resources/mdl/material-icons.css",
    "revision": "35ac69ce3f79bae3eb506b0aad5d23dd"
  },
  {
    "url": "resources/mdl/material.indigo-pink.min.css",
    "revision": "6036fa3a8437615103937662723c1b67"
  },
  {
    "url": "resources/mdl/material.min.js",
    "revision": "713af0c6ce93dbbce2f00bf0a98d0541"
  },
  {
    "url": "resources/mdl/MaterialIcons-Regular.woff2",
    "revision": "570eb83859dc23dd0eec423a49e147fe"
  },
  {
    "url": "resources/system.js",
    "revision": "c6b00872dc6e21c1327c08b0ba55e275"
  },
  {
    "url": "sw1.js",
    "revision": "0a3eac47771ce8e62d28908ee47a657f"
  },
  {
    "url": "/hoodie/client.js",
    "revision": "1d95959fa58dcb01884b0039bd16cc6d"
  }
];
const workboxSW = new self.WorkboxSW({
  "skipWaiting": true,
  "clientsClaim": true
});
workboxSW.precache(fileManifest);

The first line of the file, importScripts('workbox-sw.prod.v2.1.2.js'), imports Workbox’s service worker library. You’ll notice a new file with that name in your directory. Both files are generated whenever you run workbox generate:sw in the command line. So if you accidentally delete them, no worries.

With this new setup, run npm start in the command line and open localhost:8080 in your browser.  Open the browser’s developer console to see how the service worker is updated, allowing it to skip waiting as soon as it enters the waiting phase.

Add the app manifest

A web app manifest is a JSON file that provides information about an application (for example, the app’s name and icon) and controls its appearance in areas like a mobile device’s home screen or Windows 10 start screen. The manifest also directs which page or URL is shown when the app is launched and defines the splash screen displayed at launch.

We need to add a manifest.json file to provide metadata about the application to the user. In the public directory add a new file called manifest.json with the following content:

{
  "name": "Shopping List",
  "short_name": "ShoppingList",
  "theme_color": "#00aba9",
  "background_color": "#00aba9",
  "display": "standalone",
  "orientation": "landscape",
  "scope": "/",
  "start_url": "/",
  "icons": null
}
  • name is the name of the app that will appear on installation pop up
  • short_name appears under the app’s icon once installed
  • theme_color is the colour of the bar on top of the web app once it is opened
  • background_color is the background colour of the splash screen as it transitions between launching and loading the app’s content
  • display defines the display mode of the application. Standalone means it’ll display like a standalone application
  • orientation defines the screen orientation
  • start_url indicates the start page of your application
  • scope defines the navigation scope of the application. If the applications navigates outside this scope, it returns to being a normal web page
  • icons defines an array different icon sizes for varying devices

Notice that we left icons as null. We’ll need icons for the pin icons, push notifications, install banner, and splash screen. Download the icon at this url, which is an image with 512 x 512 as its size. Open https://app-manifest.firebaseapp.com in the browser and upload this icon so it generates the remaining file sizes we’ll need for the app.


Click “Generate .ZIP” to download a zip file which contains icons and a manifest file. We already have a manifest file, so all we need from the downloaded zip file are the icons. Unzip the file, copy the icons folder, and place it in the public folder that contains your application code. Replace the value of the icons property in our manifest.json with the following:
"icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
]

Installing our progressive web app

We now need to add a link to the manifest.json file we created in the previous section. Open index.html and history.html and add in the . Now your application is installable! You can build and start it by running npm run build && npm start again. Then open your app in a browser, it will install the latest service worker. Try adding it to your homescreen if you’re testing on mobile or adding to your desktop if you’re running from a PC.

To do this on a desktop using Chrome:

  1. Open the Applications tab in Chrome DevTools
  2. Select “Manifest” from the side menu
  3. Click “Add to homescreen”. You’ll see a popup below the URL bar. Click “Add” and watch it added to your desktop apps.

Wrap Up

We covered how to use Workbox to generate a service worker script, what a web app manifest is, and how it provides information that’ll be used when installing your app. We used https://app-manifest.firebaseapp.com to generate an icon set, but you can also add other information about your application so you can generate a manifest.json and icon files at once.
 
Now that your application is installable, you can deploy your to your preferred hosting service to make it available to the world.  You can find the complete source code for the app on GitHub.

The following resources are also great for reference:

About the Author

Peter Mbanugo is interested in offline-first and constantly seeking to learn better ways to build fast, light, and performant web apps and services. Reach him anytime at p.mbanugo@yahoo.com or @p_mbanugo on Twitter.