08.02.2018

Working with Progressive Web Apps

At Druid we are always keeping our eyes open for new technologies, which can benefit both us and our clients. Recently we have been experimenting with Progressive Web Apps. 

TL;DR

  • Progressive Web Apps provide handy app-like functionality for web applications
  • The two major requirements are the manifest.json and a service worker
  • PWAs can be the answer to user’s unwillingness to install apps on their phone

So without further ado, let’s delve into the world of manifests and service workers…

What are Progressive Web Apps?

PWA is a term for web apps that share certain features with mobile applications (i. e. phone apps).  Essentially a PWA is a web app with several modern web capabilities designed to give users a browsing experience similar to using a mobile app.

Before we get started, it should be mentioned that some of these features will not work on all phones. As of writing this blog post, iOS does not yet fully support PWAs.

The requirements

In order to be considered by browsers to be a Progressive Web App, your app must meet a list of requirements.

The most important points are these: 

  • Site is served over HTTPS
  • Pages are responsive and mobile-friendly
  • Site must work offline
  • A manifest must be provided
  • A service worker must be registered
  • Site must load fast enough for 3G

Now, most of these features that you would include anyway if you were building a web app. However, there are two important aspects, which make PWAs different than normal web apps, which I will give a brief introduction to here:

The Manifest

A manifest is a simple file which essentially tells browsers that this is a PWA. The specification can be read in details here: https://w3c.github.io/manifest/

It’s possible to define a lot of functionality in the manifest, such as a visual theme, orientation (landscape or portrait), basic configuring of colours, etc. The only mandatory fields of your manifest are name and short_name. These describe the name of your app. 

The manifest must be referred from your HTML head, like so

<link rel=“manifest” href=“/manifest.json”>

Service Worker

This is kind of the body of your PWA. It is able to process all requests to and from your site from the client’s browser. Every time the user makes a request to your site, the ‘fetch’ event is triggered and you can handle that request however you wish. 

A service worker runs in your browser (it does not have access to the DOM). The service worker is a “special” JavaScript file, which runs in a different scope than regular frontend JS. The browser can execute this script without the page being open. It can even be executed while the browser is closed. This is extremely handy for several app-like features, such as push notifications. In this article we will only scratch the basics, so we will not really be using much of this advanced functionality. 

Unlike frontend JavaScript, the service worker needs to be registered to the browser navigator.

I’ve included the script app.js in my HTML and from there the following code is executed:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('/sw.js')
    .then((registration) => {
      console.log('Service worker registered', registration);
    })
    .catch((error) => {
      console.error('Something went wrong with registering service worker', error);
    });
}

Then, in the ServiceWorker, you can add eventListeners.The most important events are install and fetch

install

This event runs the first time the user visits your site. Since it will only run once, it is expected that page load might take slightly longer than normal during this request.

const CACHE_NAME = 'static_cache';

const urlsToCache = [
  '.',
  '/js/script.js',
  '/css/style.css',
  '/images/myCat.png',
  '/images/myDog.png'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

The previous code defined the name of the cache we will be using and an array of URLs that should be cached immediately. Your cache will be created if it does not exist. You can add any internal URL here. Just keep in mind that the ServiceWorker has a certain scope. The scope is equivalent to its URL. So for example in the previous code, the service worker is located at /sw.js, which means that its scope spans the entire site. If the ServiceWorker was located at /swdir/sw.js, It would only be able to handle requests within the /swdir/ url.

fetch

Now comes the exciting part – fetch. As I mentioned earlier, this event is triggered every time (except the first) a user makes a request to your site. 

self.addEventListener('fetch', (event) => {
  console.log('Fetching data for', event.request.url);
  event.respondWith(
    caches.match(event.request).then((response) => {
      if (response) {
        console.log('Returning ' + event.request.url + ' from cache');
        return response;
      } else {
        console.log('Fetching ' + event.request.url + ' from network');
        return fetch(event.request);
        // TODO Add fetched file to cache
      }
    }).catch((error) => {
      // TODO Handle error
    });
  );
});

Remember, fetch is triggered every time a request is made for an individual file on your server. So for example, if the user visits index.html, it’s possible that a fetch event will be triggered individually for the urls index.html, /js/script.js, /css/style.css and /images/myCat.png. So in that case it will run 4 times. 

The previous code is a simple way of serving offline content. For each request, the ServiceWorker checks if the file already exists in cache. If it does, it is served to the user. Otherwise, the file is fetched from the server.

With this simple code, it is possible to have an offline PWA up and running. 

Some advice

While the concept of PWA isn’t extremely daunting or anything, there are definitely certain aspects that can be confusing and frustrating. Listed below are some of the more useful tools I used when learning the basics of PWA:

Lighthouse

Lighthouse is a built-in tool for Google Chrome designed specifically for testing Progressive Web Apps. Just go to your page, open Chrome DevTools and press the tab “Audits”. Lastly, press the blue button called “Perform an audit…”. Chrome will then run all the tests and give you the results in an easy-to-digest list. In case of failed audits, the reason for failure should be pretty self-explanatory. If you’re like me and like the shotgun approach, just keep modifying your code and re-run Lighthouse until it’s all green. Do keep in mind that your site needs to be served in HTTPS, so in case you’re testing on localhost, this audit will invariably fail. 

Google Developers

Google has some very good and up-to-date resources on PWA. One nice thing about their docs is that it will automatically warn you if the article is old and therefore more likely to be deprecated. 

Conclusion

Progressive Web Apps are a new exciting technology and it is definitely worth it to invest some resources into learning this technology. In an age where users no longer want to install apps on their mobile devices, PWAs serve as a nice middleground between the modern web applications and more traditional mobile applications.