Service Workers

What are Service Workers?

Service Worker is a new browser feature which provides event-driven scripts that run independently of web pages. Unlike other workers, Service Workers can be shut down at the end of events, note the lack of retained references from documents, and they have access to domain-wide events such as network fetches.

Going Offline

Service Workers have scriptable caches. Along with the ability to respond to network requests from certain web pages via script, this provides a way for applications to “go offline”.

Service Worker is an API that lives inside the browser and sits between your web pages and your application servers. Once installed and activated, a ServiceWorker can programmatically determine how to respond to requests for resources from your origin, even when the browser is offline.

HTML5 Application Cache

Service Workers are meant to replace the HTML5 Application Cache (AppCache). Unlike AppCache, Service Workers are comprised of scriptable primitives that make it possible for application developers to build URL-friendly, always-available applications in a sane and layered way.

Browser Support

Browser support for service worker is still behind. Google Chrome, Opera, and Firefox support service workers behind a configuration flag.

Service workers versus Shared Workers

Similarities:

  • No DOM access
  • Not tied to a page
  • Runs in its own global script context (own thread)

Differences

  • HTTPS only
  • Defined upgrade model
  • Runs without any page at all
  • Even-driven, so can terminate when not in use, and run again when used

Over HTTPS/Secure Connections only

Using service worker you can hijack connections, respond differently & filter responses. While you would use these powers for good, a man-in-the-middle might not. To avoid this, you can only register for service workers on pages served over HTTPS, so we know the service worker the browser receives hasn’t been tampered with during its journey through the network.

A Promise Based API

The future of web browser API implementations is Promise-heavy. The fetch API, for example, sprinkles sweet Promise-based sugar on top of XMLHttpRequest. ServiceWorker makes occasional use of fetch, but there’s also worker registration, caching, and message passing, all of which are Promise-based.

Implement a Service Worker

Let’s start by creating a basic app, and add offline support to it.

Registering a Service Worker

The first step is registering the service worker. In a index.js file, add following code:

if ('serviceWorker' in navigator) {

}

Here we are detecting if the feature is available.

Next, we will call .register and pass in a JavaScript resource to be executed in the context of a service worker.

if ('serviceWorker' in navigator) {
  console.log('CLIENT: registration in progress.');
  navigator.serviceWorker.register('service-worker.js').then(function() {
    console.log('CLIENT: registration complete.');
  }, function(err) {
    console.log('CLIENT: registration failure - ' + err);
  });
}

Make sure your service-worker.js file is served from the root directory, not from src or js or similar directory, because the context of your service worker will be limited to the given folder your JS file is in.

To test this, you can fire up a server using SimpleHTTPServer as follows:

python -m SimpleHTTPServer 8901

Lifecycle

Service worker script goes through three stages when you call register.

  1. fetch – this event fires whenever a request originates from your service worker scope
  2. install – this event fires when a service worker is first fetched
  3. activate – this event fires after a successful installation

Installing the service worker

A version number is useful when updating the worker logic, allowing you to remove outdated cache entries during the activation step.

var version = 'v1::';

You can use events to interact with install and activate:

var version = 'v1::';

self.addEventListener('install', function(event) {
  console.log('WORKER: install event in progress');
  event.waitUntil(
    caches.open(version).then(function(cache) {
      return cache.addAll([
          '/',
          '/css/style.css',
          '/src/index.js'
        ]);
    }).then(function() {
      console.log('WORKER: install completed');
    })
  );
});

You can pass a promise to event.waitUntil to extend the installation process. Once activate event fires, your service worker can control pages.

Intercept Fetch Requests

The fetch event fires whenever a page controlled by this service worker requests a resource.

self.addEventListener("fetch", function(event) {
  console.log('WORKER: fetch event in progress.');

  if (event.request.method !== 'GET') {
    console.log(
      'WORKER: fetch event ignored.',
      event.request.method, event.request.url
    );
    return;
  }

  event.respondWith(
    caches.match(event.request).then(function(cached) {
      var networked = fetch(event.request)
        .then(fetchedFromNetwork, unableToResolve)
        .catch(unableToResolve);

      console.log(
        'WORKER: fetch event', cached ? '(cached)' :
        '(network)', event.request.url
      );

      return cached || networked;

      function fetchedFromNetwork(response) {
        var cacheCopy = response.clone();

        console.log(
          'WORKER: fetch response from network.',
          event.request.url
        );
        caches
          .open(version + 'pages')
          .then(function add(cache) {
            cache.put(event.request, cacheCopy);
          })
          .then(function() {
            console.log(
              'WORKER: fetch response stored in cache.',
              event.request.url
            );
          });

          return response;
      }

      function unableToResolve () {
        console.log(
          'WORKER: fetch request failed in both cache and network.'
        );

        return new Response('<h1>Service Unavailable</h1>', {
          status: 503,
          statusText: 'Service Unavailable',
          headers: new Headers({
            'Content-Type': 'text/html'
          })
        });
    });
});

Let’s have a look at what’s going on here:

caches.keys()

This method returns a promise which will resolve to an array of available cache keys.

return Promise.all( ...

We return a promise that settles when all outdated caches are deleted.

.filter(function (key) {
  return !key.startsWith(version);
})

Filter by keys that don’t start with the latest version prefix.

.map(function (key) {
  return caches.delete(key);
})

Return a promise that’s fulfilled when each outdated cache is deleted.

The code snippets for this post can be found on Github.

Be first to comment

Leave a Reply