At BotSquad, we believe that Bots are the new apps. And since we support a web interface to interact with a bot, we need that web interface to be as app-like as possible. Luckily, Google has largely the same vision, which has resulted in their Progressive Web App (PWA) effort.

service workers

One of the main goals of a PWA is that the webapp should be loaded as quickly as possible, and that it should display (some) content when the mobile device is offline. For this reason, PWA states that a service worker script should be installed on the domain, which has the responsibility to cache frequently used static resources (HTML, CSS, JS, etc).

Creating such a service worker turns out to be very easy with the Phoenix framework. The mix phx.digest task has a few responsibilities. It compresses all static files, and renames them so that they get unique file names, based on a hash digest of the contents. Such dynamic filenames would be hard to parse by a generic service worker script, but, luckily, phx.digest also creates a cache_manifest.json file, which lists exactly all the static files, with their digested filenames, that the service worker could cache.

Changing the endpoint

To enable the caching service worker, we need to make sure that two additional files get served statically on our endpoint, namely sw.js, the service worker script, and the cache_manifest.json file.

Change your endpoint.ex code so these files get included:

  plug Plug.Static,
    at: "/", from: :your_app,
    only: ~w(css fonts images js robots.txt sw.js cache_manifest.json)

Adding the sw.js file

Our service worker is based on the generic PWA example, but instead of hardcoding the list of files, it loads them from this JSON file.

Create a file priv/static/sw.js which contains the following code:

// during the install phase you usually want to cache static assets
self.addEventListener('install', function(e) {
  // once the SW is installed, go ahead and cache the resources
  e.waitUntil(
    fetch('/cache_manifest.json')
      .then(function(response) {
        return response.json()
      })
      .then(function(cacheManifest) {
        var cacheName = 'cache:static:' + cacheManifest.version
        var all = Object.values(cacheManifest.latest).filter(
          function(fn) { return fn.match(/^(images|css|js|fonts)/);
          })
        caches.open(cacheName).then(function(cache) {
          return cache.addAll(all).then(function() {
            self.skipWaiting();
          });
        })
      })
  );
});

// when the browser fetches a url
self.addEventListener('fetch', function(event) {
  // either respond with the cached object fetch it
  event.respondWith(
    caches.match(event.request).then(function(response) {
      if (response) {
        // retrieve from cache
        return response;
      }
      // fetch as normal
      return fetch(event.request);
    })
  );
});

The only thing that needs to be done now is to install the service worker script when the HTML loads:

<script>
  if('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
     .then(function() { console.log("Service Worker Registered"); });
  }
</script>

And there you have it! You should see the “Service worker registered” message on the console, and in the inspector on the Application tab, you should be able to see that the service worker was registered. And when you look in the network panel, you should see From service worker reported for your static resources when you reload the page.

service workers

One gotcha: when running in development mode, the resources are not loaded from the SW cache, as Phoenix does not generate URLs with the hash digests in them on dev. So for instance, /js/app.js is requested, instead of /js/app-c4dab2a4a9e4fbf1c1e76b31b987c328.js?vsn=d" You can change the service worker to cache these other paths as well, of course, but you could also run your app with MIX_ENV=prod. The README file of the example code gives an instruction on how to test the SW properly.

Trying it out

Now you want to see whether your webpage loads when you are offline, of course. Tick the “offline” button in the Service Workers inspector and hit reload. And… nope. Chrome gives an error message! Why’s that? Because the HTML page itself was not cached in the SW, because the HTML is not considered a static file by Phoenix.

Luckily, this is easy to change. Add the URL of the HTML you want to cache to the all list in the service worker file:

all.push('/')

And now, when we re-install the service worker, it should be fine when loading offline. Want to try it yourself? All code for this blogpost is online on the phoenix-service-worker-example repo on Github.

Enjoy building your PWA!

Arjan Scherpenisse

Arjan Scherpenisse

Co-founder Botsquad

email me