More PWA to Ya! (Progressive Web Apps, Part 1)

Image of power source at sunset

It’s project kickoff time, and you’re having a conversation with your client about what form the application will take:

Client: I’m thinking mobile app. Our users will definitely be using this on the go.
Dev: Sure, we can do a native mobile-
Client: Mind you, we’ll want a desktop version too. We’ll need to use it from the office.
Dev: Okay, well, a responsive web app-
Client: One of our priorities is definitely ease of access – we’ll need the app accessible from the home screen, ’cause who has time for typing in URLs, amirite? We’ll also want it to be useable offline, whenever people want to.
Dev: Ye-yeah, no problem, we can wrap your web app in a webview, bundle it up as a native app, and-
Client: Yeah, cool. So they’ll just be able to go to the site and install the app, right?
Dev: Well, no, they’ll have to download it from the appropriate App Store.
Client: Eh, that’s a no-go – this is internal only, we can’t have it showing up in the app stores. Didn’t I make that clear from the start?
Dev: …

The term your client was looking for is Progressive Web App – an application that acts like a responsive web app when accessed from the browser on any device, but can be installed to mobile devices like a native application. The link above makes the case for PWAs, so we won’t belabour the point – if you’re still here, it’s because you’re convinced it’s time to build a PWA.

Let’s dig into the details. We’re going to assume you have, or are building, a responsive or mobile-focused web application, and want to convert it to a PWA. Keep in mind that, like wrapping a webapp in a webview, all the heavy lifting is still done by you, the developer, in CSS, HTML and JS – there’s no PWA magic to make it look ‘native’.

Well, actually, there is a little magic. We’ll get to that next time.

Part 1 will focus on implementing a PWA the standards-compliant way. In Part 2, we’ll address the ‘little bit of magic’ PWA’s can have on Android to appear more native, and PWA’s on iOS Safari, because it always has to be a special snowflake.

Let’s begin.

Service, Please

Familiar with Service Workers? If not, get ready to do some reading on them – Service Workers are the clockwork that make PWAs tick.

Mozilla’s summary of Service Workers make it clear how this is so – and also, how complex a Service Worker can be:

Service workers essentially act as proxy servers that sit between web applications, and the browser and network (when available). They are intended to (amongst other things) enable the creation of effective offline experiences, intercepting network requests and taking appropriate action based on whether the network is available and updated assets reside on the server. They will also allow access to push notifications and background sync APIs.

If you’re familiar with Web Workers, you’re about half-way there – Service Workers run on their own ‘thread’ (actual implementation details are up to the browser, of course), and have no access to the DOM, like a Web Worker. They have significantly more power than a web worker, however, particularly in terms of interacting with network requests; and, as a result, have more requirements to meet.

Let’s run down the checklist, and then we’ll get into some implementation details. For a PWA’s Service Worker you will need:

  • A secure context – Service Workers must be run from a TLS-secured domain (https), because they can be Men-in-the-middle on every request from the browser for a given domain. The localhost special-case domain is the only exception to this rule, for the sake of development.
  • A full list of the items you need to cache (for the caching and serving from cache functionality of a Service Worker, which is a requirement to have your application considered a PWA). Wildcards can’t be used – you need to give the full (relative) path of the resource you want cached. If you’re familiar with the Application Cache API this restriction will be familiar to you. For your application to be considered a PWA, at least the start url must completely load when the user is offline.
  • A means of serving the Service Worker from the root of the path that you want the Service Worker to have control over – so, if you want the Service Worker to be able to control and serve resources for your entire application, and your application is at https://app.example.com/, you will need to be able to serve the Service Worker from / (as opposed to, e.g. /static/js/workers/ – if you serve from there, the only resources the Service Worker will be able to control will be those under that path).

Additional checklist items for a PWA include:

  • A responsive (or mobile-focused) design.
  • Quick initial load – Google, the company behind the original PWA spec (you may have heard of them), strongly suggests that your start url load under 10 seconds on a simulated 3G network – so no loading a half-dozen affiliate advertisements.
  • You will want to consider making your application a SPA – it’s generally a good fit for this use case, especially if it allows you to cache more up-front, or trim down the number of bits your application needs to transfer over the network.

Looking for a Hard Worker, Room and Board Provided

First things first, let’s set up our web server to serve that Service Worker (lot of variations on ‘serve’ in that sentence) from the root of our domain, so we can control all resources and requests therein. We’re going to assume you’re using Python and Django in the example below, but the principle will always be the same.

First, we’ll create a service worker named
CacheWorker.js
in our
static
directory, under a
serviceWorkers
directory. Then, we can create the view to serve this worker (and any others we might want to serve with potential control over all requests):

Next, in urls.py, we’ll add the route to this view:

Now, assuming our domain was
https://app.example.com
, a request to
https://app.example.com/worker-cacheWorker.js
will return our worker script.

Putting Your Workers in their Place

Now that we’re serving our worker script from the desired location, we need to tell the browser that it should be requesting said worker script, and installing it as a Service Worker.

To this effect, we will want to use the ServiceWorkerContainer API to register our service worker. Of course, since our PWA is a progressive enhancement, we will check to ensure that the browser actually supports Service Workers before we try and install it – your application should have some fallback behaviour when it encounters a browser that doesn’t.

Cache Me, I’m Falling

So, we have our server sending our cacheWorker file along properly, and we have the browser registering the service worker, and downloading and installing our script. That’s great – except, our cacheWorker script is empty, so it doesn’t do anything. Let’s fix that.

So, that’s a lot to take in, but the comments above should help. Let’s break down a few of the more complex bits:

Install

So, you’ll notice that we have add an event listener for ‘install’. This is the event that gets fired when a new version of the Service Worker is installed – whether for the first time, or because the Service Worker has changed in some way. The browser does a byte-wise comparison of the downloaded script, so any change will trigger a re-install of the script.

Often, you won’t need to change anything in the script itself, but some of the cached resources have changed, and we need to tell the browser that we want to refresh the cache. In order to do this, we’ll change the
cacheVersion
number, which will trigger the browser to re-install the Service Worker, triggering our install event, and allowing us to re-downloand and cache all of the updated resources.

Activate

After that, we have a listener on the ‘activate’ event. This gets triggered when our service worker takes control of the context – once it becomes ‘active’. You’ll notice in our ‘install’ event that we’re calling
self.skipWaiting();
. The normal behaviour is that when a new Service Worker is installed, it doesn’t take over from the old Service Worker until the page is refreshed. This could be fine for your case, but in many cases, we’ll want to start serving the new resources right away, so we tell the browser to skip waiting for this Service Worker, and activate it immediately, allowing it to take over from the old Service Worker (if any).

In the activate event listener, we then clear out all of our old cache versions, leaving just the specified cache available. You’ll want to take a look at the Cache Interface for more details here.

Finally, once we’ve cleared out the old caches, we post a message back to main thread of any clients we have control over that they should reload, since the cache has been updated (remember the
handleMessage
function in the install workers script?). What you want to do with this message will depend on your application, but it could be as simple as calling
location.reload()
to get the new resources from the Service Worker.

Fetch

And here’s where the actual offline-enabling functionality happens. You’ll want to take a look at the Fetch API for more details on fetch, but the point here is that we’re intercepting all requests from the client (that’s the browser main thread for our origin), and checking the requested resource name against our cache. If we have the resource, we return it from our cache, and otherwise we pass the request through to the network.

Two things are worth noting here:

  1. Our fetch intercept catches all requests – not just XMLHTTPRequests, for example, but every single request the main thread makes of the network. There’s a lot that can be done with this – we’re just scratching the surface here.
  2. The ‘serve from cache if available, and otherwise pass to the network’ is just one potential model for how we can handle caching and serving resources with the Service Worker – see this article on Caching File with Service Worker for some alternative approaches.

Are We There Yet?

Pretty much! Assuming you can mark the other items off the checklists above, your application should now be ready to serve itself as a PWA. To confirm this, you can use the Lighthouse Extension from Google to test your site, and confirm that all is as it should be.

While this gives you a PWA suitable for, say, desktop use, there’s still a few pieces to the puzzle before we’re ready to be assigned real estate on the home screens of Android or iOS devices – we’ll be going into that next time.

Header image by kappuru, licensed under Creative Commons BY-NC-ND 2.0.

Christopher Keefer

Christopher Keefer

Christopher Keefer is a Senior Software Engineer at Art+Logic. He generally spends his spare time on the computer too, so there isn't much hope for him.
Christopher Keefer

Latest posts by Christopher Keefer (see all)

Tags:

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.