December 4, 2018

Progressive Web Applications: Part 2

by Dan Korycinski

In the article "Progressive Web Applications, Part 1," I laid out what a progressive web application is, and how it can enhance a web application to look and function like a real, native mobile application. I also covered what is and isn’t supported by today’s major platforms, Android and IOS. In this article I’ll be showing the technical details involved in creating a PWA.

There are really only two technical requirements to implementing a progressive web application – a web application manifest and a service worker.

The manifest is a file that contains settings for how things should look or function once the app is installed. This includes things like the text to show near the application icon, the color of the splash screen, and the icons that can be used to represent the app.

The service worker is a script file that can implement app functionality like offline caching and serving of data, and clean up old service workers, and handle push notifications.

Web Application Manifest

About the bare minimum for a manifest file is:

{

    "name": "My PWA Application",

    "short_name": "MyPWA",

    "start_url": "/",

    "display": "standalone",

    "icons": [{

        "src": "/assets/images/icon192.png",

        "sizes": "192x192",

        "type": "image/png"

    }, {

        "src": "/assets/images/icon512.png",

        "sizes": "512x512",

        "type": "image/png"

    }]

}

This tells the app what to do once installed. Additional icons of different sizes can be included as well, but Google recommends the 192x192 and 512x512 icons as a bare minimum as they can be sufficiently scaled on most devices. Mozilla has a detailed spec for the manifest here, but the following list covers what each of the properties do in the above file:

  • name: long version of the application name that will be used in places where there is more screen real estate. Android should use this during the app install prompt.
  • short_name: shorter version of the name used in more constrained areas. Android should put this name by the application icon on the home screen.
  • start_url: tells the device what URL or route should be opened in the app when first launched.
  • display: dictates how the application is displayed on a device after install. Options are fullscreen (fullscreen app which hides all device menus and buttons), standalone (looks like a normal app with no browser elements), minimal-ui* (similar to standalone, but should show basic navigation buttons like back and forward), and browser (this opens the app in a browser, but it still has all the same offline functionality).
  • icons: list of icons available on the server at various resolutions. Google recommends at least 192x192 and 512x512, but more can be provided for exact resolutions so devices don't have to auto scale the images themselves.

* minimal-ui isn't supported by any browsers that I can see, but is still in the spec

Once the manifest is created and accessible on the web server, the app HTML must be updated to include a pointer to the manifest file. This is done using a <link/> tag in the <head> of the app’s main HTML. If the file is named “manifest.json” and is in the root of the application the link might look like this:

<link rel="manifest" href="/manifest.json">

Service Worker

The service worker is where things start to get interesting. The core service worker functionality happens in the install, activate, and fetch event subscriptions. These events are specific to service workers and handle the basic functionality needed to create an offline application.

install

The install event is just as it sounds. This is where the application starts installing. It’s the first event to occur in the service worker and is typically where files will be downloaded from the web server and added to the application cache. The caches API is used to open a cache with a specific name and then add a list of files from the server. Here’s a simplified example of an install implementation:

var cacheName = ‘my-service-worker-cache-1’;

 

self.addEventListener('install', function(e) {

    var cacheFiles = [

        '/index.html',

        `/css/site.min.css`,

        '/js/site.min.js'

    ];

 

    e.waitUntil(caches.open(cacheName).then(function(cache) {

        return cache.addAll(cacheFiles);

    }));

});

The service worker is opening a new cache with the cacheName variable and adding all the files from the list. Pretty simple! Alternatively you may choose to handle caching manually by using fetch to retrieve files and handle errors that might occur in the network request.

It’s typically important to store the cached files under a new, unique cache name during the install process. In most of the implementations I’ve seen this is usually a string with some numeric value like my-service-worker-cache-1 which is incremented by the developer each time a new service worker is released. This makes it easy to clean out old caches when a new service worker is installed and if new resources like images need to be loaded. This simplifies maintenance from a developer perspective.

You might notice that the event handling is added using self.addEventListener. The self variable is actually predefined in the scope of a service worker. There’s no need to set var self = this. It simply exists automatically.

activate

The activate step is where the service worker instance essentially goes live. This is separate from the install step because it might not happen right away. The first time an application’s service worker is installed it should activate right away, but during updates the service worker won’t activate until all instances of the application are closed.

Since this is when a service worker officially takes charge it is also typically where applications will clean up old caches and files. This is something developers need to be proactive about. Cached items eat into the total amount of offline storage available to the application, and old caches won't be cleaned up automatically. Service workers will need to delete old cached files while keeping the files that were cached for the current service worker (during the install phase) in the cache.

The below code shows how to get the list of caches and deletes all caches except for the cache created by the current service worker.

self.addEventListener('activate', function(e) {

    e.waitUntil(caches.keys().then(function(keyList) {

        return Promise.all(keyList.map(function(key) {

            if(key !== cacheName) {

                return caches.delete(key);

            }

        }));

    }));

 

    return self.clients.claim();

});

The final line here (return self.clients.claim()) is an optional piece of code. Without it the service worker might not take control of the current app instance until the next reload. It may be “active,” but still not running. self.clients.claim() should tell the app that the service worker can officially take control (important for the next event).

fetch

The fetch event occurs whenever any scripts use the fetch API to make requests. This event gives a service worker the opportunity to intercept the request and choose how to respond, and is what gives the app the ability to work offline once installed on a user’s device. There are a number of different strategies that could be implemented here depending on the situation:

  • Serve the request from the cache or network only
  • Make an attempt to serve all requests from cache, but fall back to a network requests if not found in the cache
  • Serve all requests from the network in case there is a newer resource on the server and fall back to cache when offline
  • Make requests to both cache and network and use whichever one returns first

I created a simplified version of a fetch handler below that opens the cache, attempts to match the current request to an item in the cache, then responds with either the cached value or a new fetch request over the network.

self.addEventListener('fetch', function(e) {

    e.respondWith(caches.open(cacheName).then(function(cache) {

        return cache.match(e.request).then(function(response) {

            return response || fetch(e.request);

        });

    }));

});

Registration

The code above will all typically go together in one file and be hosted alongside a web application. There is one rule here–the service worker will only be able to handle requests to resources at or below it in the hosting hierarchy of the application. For instance if your service worker file is hosted at https://example.com/app/SW.js it won’t be able to intercept requests to https://example.com/assets. In many cases it will make sense to host the service worker at the root of the application, so in the example above placing the service worker at https://example.com/SW.js would give it the ability to intercept and handle requests for the entire application.

Finally, the following code should be placed somewhere in the main application’s JavaScript to install the service worker (assuming the service worker file is called SW.js and is hosted at the root of the application):

if('serviceWorker' in navigator) {

               navigator.serviceWorker.register('/SW.js);

}

There’s not much to explain here–but this keeps with the progressive approach by checking if the serviceWorker object is even available on the current window navigator and, if so, registers the service worker file for the application.

Summary

The above examples cover the basic functionality needed to create a progressive web app. Compared to developing a native application, it’s relatively easy to extend a web app with these components to provide offline functionality and more. Service workers can even implement push notifications that can be received whether the app is open or not.

If you’re looking for more resources to learn or want to see some live examples, one great resource for exploring PWAs is the Hacker News PWA site. If you’re not familiar, Hacker News is a popular social news outlet that posts computer science-related news stories. They have a public-facing API hosted on Firebase that makes it easy to write a front end UI to their news feed. The HNPWA spec is designed to give developers a simple set of requirements for building an app that can showcase the functionality of server and client-side libraries in building a PWA. Developers who implement the spec typically post the code on GitHub and publish the app somewhere for reference. There are a lot of great examples there which show PWAs implemented in a lot of different frameworks.

If you're looking to hire technical staff or are interested in custom software tailored to your business' needs, please contact us or explore our services to learn more. You can also sign up to receive our technical and business articles straight to your inbox.

info@stoutsystems.com
877.663.0877
© Copyright 1995-2018 - STOUT SYSTEMS DEVELOPMENT INC. - All Rights Reserved
envelopephone-handsetlaptop linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram