Writing offline-first web applications with Service Worker API

2017/01/04

JavaScriptServiceWorker


With more devices connected, the Internet is more and more required resource. Web applications are providing rich content, not only written information. But what if the connection is not available, shouldn't you be possible to access information that was already provided to you? Writing applications that are optimized for offline usage increases user's experience, thus usage of your application.

Are you familiar with devdocs.io? It's my "the most favorite page" last year not only because it aggregates most of the documentation I use on a regular basis, but also because it's available offline. How does one achieve that? Well, you can simple use Cache API available in modern browsers.

For example, you would like to cache the responses of requests that fetch static assets (e.g. images, styles, scripts), so that you can view them when offline. It is described in the next image:

Using Cache

In order to load the site, browser calls a GET request (1) that as a response (2) returns static asset, which is put in cache and also used to render the page. Once the network is not available (3), the response comes from cache (4).

Using the Cache API is, however, awkward, it would require a lot of overhead just to manage the persisting and updating cache. The solution to that is to use Service Worker API. You can read the introductory article, which basically describes everything you want to know.

But let's return to our real-world-example. The frontend app will "cache" itself when install event is processed by service worker. Only after that it will allow the rest of the application to be processed (activate event).

Service Worker registration and activation

Now, we will force the application to read from the cache first and only if it is not found the resource is loaded form the internet (e.g. cache first strategy for static resources).

Service worker read from cache

We will implement this in an Angular 1.6 application to view images from a gallery. The service method that fetches the images from gallery has a fallback to show just a "image not found" temporary image if request is not succesful:

/src/app/slideshow/slideshow-service.js

...
getPhotos(page = 0) {
  return this.httpService
    .get(`/api/random-image/list?page=${page}`)
    .then(result => result.data)
    .then(images => images.map((image) => new Image(image.name, image.url)))
    .catch(() => [new Image('', '/img/notfound.png')]);
}
...

In order to make /img/notfound.png available offline we need to cache it.

/src/app/service-worker.js

const CACHE_NAME = 'angular-test-cache';
const STATIC_ASSETS = [
  '/',
  '/app.bundle.js',
  '/img/notfound.png',
  '/img/favicon.ico'
];

self.addEventListener('install', (event) =>
  event.waitUntil(
    caches
      .open(CACHE_NAME)
      .then((cache) => {
        log(['Static assets added to cache: ', STATIC_ASSETS]);
        return cache.addAll(STATIC_ASSETS);
      })
  )
);

Lastly, we need to intercept the fetch event, in order to return correct response (cached "notfound" image):

/src/app/service-worker.js

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches
      .match(event.request)
      .then((response) => {
        if (response) {
          log([event.request.url, 'Retrieved from cache']);
          return response;
        }
        return fetch(event.request).catch(() => log(['An error has occured']));
      })
  );
});

You can find the full sources on my github page. Event though the backend logic is missing (websocket and image-api) you will be able to start (npm install && npm start) the application.

This topic was also part of my TechTalk at Itera so you can view the slides from the presentation as well.