When was the last time you opened the App Store on your phone? No, I don't mean a website that took you to the App Store, I mean that you unlocked your phone, specifically went to the App Store, and looked for new interesting apps to install. I'll speak for myself; it has been so long; I don't even remember.

Let's be honest, the novelty of the App Store has worn off. Whenever Apple releases a new iOS version, with a significant new capability that's locked to new kinds of applications, I may look at it out of a curiosity perspective. But, the process of downloading, installing, and then cleaning applications is just tedious. I think the most commonly used app on my phone is the browser.

However, browser-based applications, or let's call them websites, have significant limitations. On the other hand, native apps that get installed on your device are a hassle to install, manage etc. Progressive Web applications are somewhere in the middle. They are built using browser-based technologies, but they can act and be installed as a native app. They can be installed directly from your browser, or even distributed through the App Store. Realistically speaking, although they can work offline, do interesting things like offline storage, work with your camera, do geolocation, and typically do things that are reserved for a native application, a native app will still win when it comes to those capabilities. Progressive Web apps give you the incredible convenience of being installable right through the browser or App Store, if you choose, and they are built using standard Web-based technologies that we're already familiar with. And although progressive Web applications may not scale to the capabilities of a native application, a vast number of applications don't need those capabilities.

In fact, you'd be surprised to know how many applications in app stores today are actually progressive Web apps (PWAs). In this article, I intend to explain what PWAs are, their capabilities, and their limitations.

Before I get started, let me let you in on a little secret: Even if you never intend to write a mobile application, the concepts presented in this article are applicable to you as a Web developer. The amazing thing about PWAs is that they simply use Web-based technologies that you're already familiar with. For example, one of the core technologies of PWAs is service worker. Service worker, when applied to any Web application, can greatly improve the load times of your Web pages. Research after research has shown that users prefer to use fast loading Web pages. In fact, if your Web page takes more than three seconds to load, that might be a lost customer. That's just one of the things a PWA can do for you. I was amazed to open my developer tools on my browser and see how many applications already use these technologies.

Without further ado, let's get familiarized with core building block of PWAs, the service worker.

Service Worker

Sometimes, it's hard to be a worker in the IT industry. For example, if I were to use the words “service worker” at a party, what would people think? So, before anyone casts any aspersions, let's go ahead and define what a service worker is. A service worker is a client-side programmable proxy written in JavaScript between your app and the outside world. It can give you a lot of fine control on your network requests. This means that you can control the caching behavior of individual components of your Web page or website. This gives you a lot of control on what gets loaded and what doesn't. For example, you can decide that large JavaScript files are loaded only on demand, and they are cached. This means that when the user revisits your Web page or goes from page to page within the same website, the files are not constantly reloaded. This is not so great for complex pages that you want to load faster.

A service worker is a client-side programmable proxy written in JavaScript between your app and the outside world.

Additionally, service workers also enable you to handle push messaging. A service worker is nothing but a type of a Web worker. It's a JavaScript file that runs separately from the main browser thread. It intercepts network requests, caches or retrieves resources from the cache, and can deliver push messages to you.

The key here is that the service worker runs separately from the main thread. This means that service workers are independent of the application they are associated with. This means that they're a lot more powerful than the traditional caching techniques that you've used in the applications you've used so far. Another consequence of the service worker running separately from the main thread is that it can receive push notifications even when the app isn't running in the browser.

Service worker APIs are also designed to be fully asynchronous; this means that synchronous APIs, such as localStorage, are inaccessible from service workers directly. Additionally, they must work only on HTTPS. A service worker can't access the DOM directly, but must work through the postMessage method to send data and a message to an event listener to receive data.

As you can imagine, service workers, given their nature of being able to run independent of the application itself, open doors to some amazing capabilities. Some of these capabilities are offline; first application pattern and leveraging caching, a Notifications API that allows your application to interact with the operating system's notification system, a push API that lets your app subscribe to a push service of any kind, a background sync API that lets the user defer actions until better network connectivity is available, and a channel messaging API that service workers communicate with the main application, so the user can be prompted for actions when new and interesting information becomes available.

Let's understand each one of these one by one.

Notifications API

The Notifications API allows your Web application to push notifications to the user by taking advantage of the operating system's notifications capabilities. You may have noticed that on any modern operating system, applications can request to send you notifications when something of interest appears for you. These notifications can then be centrally managed in control panel, system preferences, settings etc. Wouldn't it be nice if a Web application could send such notifications as well? Of course, this needs the user's permission. That's exactly the job of the Notifications API. You'll be pleased to know that using the Notifications API is really simple. Here is how you use the Notifications API.

let promise = Notification.requestPermission();

Notice that a lot of these APIs rely on a promise-based mechanism. This is because these APIs are designed to be asynchronous. For example, the Notifications API returns a promise, which then waits for the user to either allow or disallow sending notifications. This dialog asking for permission is specific to the operating system and browser you're on, but on Mac and Safari, it looks like Figure 1.

Figure 1: The Notifications API requesting permission
Figure 1: The Notifications API requesting permission

The Notifications API comes with a rich set of methods and properties that let you do the various things you'd expect from a first-class notification infrastructure. For example, it lets you check whether the user has granted a permission or not. It lets you craft up a notification and it lets you show the notification. It then gives you the various events necessary to handle the notification, for example whether it's visible, whether it has been dismissed etc. In scenarios where the user hasn't granted permissions for notifications, you can re ask the user to grant such a permission, or simply show an alert box or some other mechanism for communicating with the user.

Push API

The push API allows your application to receive messages pushed to it from the Web server. Note that this is different from your standard Ajax calls. The push API allows your application to receive messages when the application isn't even loaded. The push API works hand-in-hand with the Notifications API; for example, a pushed message can be shown as a notification.

For your application to receive push messages, it must have an active service worker. When the service worker is active, you simply subscribe to push notifications using the below code snippet:

let promise = PushManager.subscribe();

This gives you a PushSubscription object, which has all the information, such as the subscription URLs endpoint, expiration time of the push subscription, etc. Realistically speaking, to use the push manager, you'll always need a service worker. So once you have a service worker registration, your code will look like this:

serviceWorkerRegistration.pushManager  
                         .subscribe(options)
                         .then( function(pushSubscription) {   
                             // use the subscription 
                         });

Background Sync API

The background sync API allows you to defer actions until the user has stable connectivity. As you can imagine, this is critical for occasionally online applications. Remember that in a lot of places in the world, there isn't reliable or good internet connectivity. This isn't a binary one or zero, as in being online or offline. Sometimes when you have poor connectivity, you want to pull the phone out of your pocket, take a picture, and put it back in your pocket. Later when you have better connectivity, you can perhaps upload that picture. This is exactly what the background sync API lets you do.

Just like many other APIs, the background sync API also relies on service workers. It uses a service worker as an event target, which allows it to work in a background mode when the Web page isn't open. You use it in two steps: First you register your service worker and listen for sync events from a page, and second you listen to the event in your service worker.

To register, you use this code:

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

Once registered, you can then request for a sync:

navigator.serviceWorker.ready.then(function(registration) {
    return registration.sync.register('syncEvent');
});

Later, when there's a sync event matching the phrase “syncEvent”, you can act upon it in your mysw.js file as shown here:

self.addEventListener('sync', function(event) {  
    if (event.tag == 'syncEvent') {    
        event.waitUntil(doStuff());  
    }
});

The doStuff method returns a promise and that's where you can work on the actual task. If the promise fails, another sync is automatically scheduled for you. Retry sync waits for connectivity and also uses exponential back off.

Now, don't let the name background sync fool you; this is actually a very useful feature that can be used for more than just syncs. For example, imagine that you were writing a chat program. You could use background sync to alert the user of a new chat message. Think of it this way: Anytime you have a very small bit of information that you want to push from the server back to the client, even when the user has navigated away from the page, that's where background sync is your friend.

Channel Messaging API

How often you have two different browsing contexts, for example two different iframes that need to communicate with each other? That's exactly the problem that the channel messaging API solves. The way this works is that you create a message channel using the MessageChannel() constructor. Once the message channel is created, the two ports of the channel can be accessed through port1 and port2 properties on the MessageChannel object. The app that created the channel uses port1 and the app on the other side uses port2.

Service Worker Lifecycle

In order to use service workers effectively, the first thing that you need to learn is the service worker lifecycle. There are three main steps that a service worker goes through in its lifecycle: registration, installation, and activation.

Registration

This is the first step you need to do in order to use a service worker. Registering a service worker tells the browser where your service worker is located and to start installing it in the background. The code shown in Listing 1 shows how to register a service worker.

Listing 1: Service worker registration

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function (registration) {
    console.log('Registration successful, scope:', registration.scope);
  })
  .catch(function (error) {
    console.log('Registration failed, error:', error);
  });
}

The code shown in Listing 1 uses a file called sw.js where the actual service order code lives. You can also choose to register your service worker at a well-defined scope as can be seen here:

navigator.serviceWorker.register('/sw.js', {scope: '/app/'});

By registering a service worker at a well-defined scope, as is the case here, is on the /app/ scope. This service worker intercepts requests to /app/* URLs.

There's an additional setting you need to do on the server side as well. The server side needs to include a Service-Worker-Allowed header matching the scope you've defined.

Installation

If the service worker registration succeeds, you can attempt to install it next. This would generally be dumb if you're attempting to install a new service worker at a scope that didn't previously have a registered service worker, or you are installing a new service worker on a previously installed scope.

Installing a service worker simply means that you need to listen to the “install” event. This means that you listen to the “install” event listener, in the service worker, perform some task once you are informed that the service worker is done installing. An example usage of this could be, hey, my service worker is now installed, let me go ahead and pre cache some portions of the page so the next time the user opens this page, it reads from the cache. The next code snippet shows you how you can listen to the installation event and act upon it. Remember, this code goes in the sw.js file (or whatever service worker file you have chosen).

self.addEventListener('install', function (event) { // do work });

Activation

There's a possibility that when you install a service worker, you may be replacing a service worker in an existing scope. For any given scope there can be only one service worker active at a time. When you're replacing an existing service worker, the existing service worker needs to finish working. The new service worker enters a waiting state. In such an instance, when the new service worker finally activates, an activate event is triggered in the activating service order. This event listener is where you can start performing a task. Listening to this event listener is as simple as the code shown below.

self.addEventListener('activate', function (event) { // do work });

With these basic fundamentals understood, now let's try to solidify your understanding with an example.

A Simple Offline Application

So far in this article, my focus has been around the service worker. Believe it or not, service worker is just one aspect of PWAs. There's so much more to learn, and I hope to talk more about those in future articles, but meanwhile, let's solidify your understanding with what you've learned so far with a simple example application that works both offline and online with the help of service workers.

Although the concepts of PWAs are specific to HTML in JavaScript, you do need a Web server to understand what's going on here. You can choose to use any Web server. I've chosen to use NodeJS, although I assure you that the NodeJS parts are the simplest. Feel free to replace the NodeJS parts with any other Web server you wish.

To keep my demo clean, let's get the NodeJS parts out of the way first. Go ahead and create a new directory on your disk that will hold the solution. In this directory, run the following command to give yourself an empty package.json file.

npm init -y

Now you're going to need a simple Web server, so I'll go ahead and take a dependency on express js, which is a very popular node package used to create simple Web applications. Use the following command to install express JS.

npm i express 

Now go ahead and create a file called server.js, and in this file, go ahead and place the code, as shown in Listing 2.

Listing 2: The simple Web server

const express = require('express');
const app = express();
app.use(express.static(__dirname));
const server = app.listen(8000, () => {
  const host = server.address().address;
  const port = server.address().port;
  console.log('Listening at http://%s:%s', host, port);
});

As can be seen in Listing 2, you've created a very simple Web server. Now any files that you put in the root of the project will be served over port 8000. This isn't a production quality Web server, but it'll get the job done for what you need here.

And here is where NodeJS-specific stuff ends. You can replace everything I talked about here so far with any Web server you wish. Now let's focus on the PWA parts that are entirely client side.

Now go ahead and place an index.html and sw.js file in the same folder. The index.js file is where the simple application will live, and the sw.js file is where the service worker will live.

In the index.html file, go ahead and place the code, as shown in Listing 3.

Listing 3:The index.html file registering the service worker.

<!doctype html>
<html>
<body>
  <h1>My awesome page</h1>
  
  <script>
    if ('serviceWorker' in navigator) {
        window.addEventListener('load', () => {
            navigator.serviceWorker.register('sw.js').then(sw => {
                console.log('Service Worker is registered', sw);
            }).catch(err => {
                console.error('Service Worker Error', err);
            });
        });
    }
  </script>
</body>
</html>

if you pay close attention to Listing 3, the HTML page has a simple message. It says, “My awesome page.” However, there's an interesting JavaScript snippet there that registers a service worker. The service worker is defined in the sw.js file.

In sw.js file, you do two things. First, you listen to the install event. Once the service worker has installed, you cache the files that are of interest to you. These files are cached because later, they'll allow the application to work offline. In this case, you intend to cache only the index.html file, which is the only file in your project. You can, of course, extrapolate this to other scenarios. This code can be seen in Listing 4.

Listing 4: Caching files after installation

const filesToCache = ['/', 'index.html'];
const staticCacheName = 'mycache';
self.addEventListener('install', event => {
    console.log('Installing service worker');
    event.waitUntil(caches.open(staticCacheName).then(cache => {
        console.log('Caching file');
        return cache.addAll(filesToCache);
    }));
});

Now that the files are cached, the next thing you do in the service worker is listen to the fetch event. In the fetch event, if the file is available in the cache, you simply serve it from the cache. This saves a server roundtrip, and it allows the application to work offline. The code for this can be seen in Listing 5.

Listing 5: Serving files from the cache

self.addEventListener('fetch', event => {
    console.log('Fetch event for ', event.request.url);
    event.respondWith(caches.match(event.request).then(response => {
        if (response) {
            console.log('Found ', event.request.url, ' in cache');
            return response;
        }
        console.log('Network request for ', event.request.url);
        return fetch(event.request)}).catch(error => {
            console.log('Error, ', error);
        }));
    });

Now go ahead and run your application using the following node command. If you've chosen to use a Web server other than node, feel free to serve the files over a URL.

node server.js

Once the server is running, open your browser and visit localhost:8000. I'll use Chrome, but really any browser that lets you debug service workers will do. Your page should load as shown in Figure 2

Figure 2: My HTML page is working.
Figure 2: My HTML page is working.

Now open the development tools of your Chrome browser and visit the application tab. Inside the application tab, look for service workers. You should see something like that shown in Figure 3. This shows you that your service worker registered. You can also use this development tool to send simple push or sync messages if you wish.

Figure 3: Service worker debugging the UI
Figure 3: Service worker debugging the UI

Now switch over to the network tab and choose the drop-down indicated in Figure 4 to emulate offline mode.

Figure 4: Emulating the offline mode
Figure 4: Emulating the offline mode

At this point, Chrome is practically offline. Now go ahead and hit refresh on the page. What do you notice? You'll see that the page loads just as expected, except that the network call never made it to the Web server. You can verify that by setting a breakpoint in server.js, and the breakpoint won't get hit. This is great, because now the application is working offline. It's working because the service worker has intercepted the request and is serving index.html from its local cache. This is how the world's simplest PWA is built.

Summary

Let me be honest here, PWA is a groundbreaking technology. If you examine your Chrome developer tools, under the application tab, under service worker, you'll be amazed to see how many websites you commonly visit are already making use of such concepts even if you don't recognize them as applications. These are websites that you use daily. You'd also be amazed to see how many applications in the App Store are already using these concepts. And that all modern browsers today implement these concepts on all platforms.

My point is that you don't have to go full-blown PWA to start taking advantage of the involved technologies. I must also confess that this article is at a very beginner level for PWAs. There's a lot more to learn: there are concepts, such as how to design your applications in a responsive manner; there are tools available, such as lighthouse, that allow you to analyze your PWAs; there are concepts around debugging your PWAs; and then there are many other APIs, such as credential management API etc., that I didn't even get to touch upon.

In my future articles, I hope to talk more about such additional aspects of PWAs. Until then, happy coding.