read

In November 2015 at the great beyond tellerrand conference in Berlin I saw multiple talks about Service Workers. Most notable the following talk by Jake Archibald: "Modern Progressive Enhancement" featuring Service Workers.

(if you don't want to watch the full 40min talk above, he also did a short 11min video on Youtube containing the most interesting part from his talk)

Service Workers promise exciting new possibilities especially for offline usage of websites. I couldn't wait to try it myself.

What my Service Worker does

At first we put some content into our cache. So this content will always be available:

  • /offline – This is a normal HTML page with a generic text like: This page is not available. Please check your internet connection.
  • Some static assets like CSS, JS, images.
  • A fallback image that will be served in case the browser requests an image while the connection is lost. (If your site only uses background-images with something like background-size: cover and imgs that will be scaled up when they are too small, you can use a 1×1px PNG with a nice color. This worked really well for me.)

Then we listen for fetch-events (when our webpage requests something). We distinguish between the following cases:

  • If it is a non-GET request, we do "nothing" (we tell the browser to do what it would do without Service Workers)
  • For HTML requests we request it from the server, deliver it to the webpage and put it into our cache. In case this network request fails, we try to get an old version from our cache and show the user an offline-message (something like "You are offline. This page might be outdated."). If we don't have that page in our cache yet, we deliver the /offline page (the one we initially put into our cache).
  • For non-HTML requests (static files like images, CSS, JS) we first try to get the content from our cache. If we don't have it in our cache, we request it from the network. If the network fails & the request type is an image, we deliver the fallback image, we initially put into our cache.

How it works

This is the final result of my first experiments with Service Workers, so it might look a bit overwhelming. I tried to insert some comments for better understanding what the code does. Nevertheless I recommend for beginners in this topic to read this article by Jeremy Keith first. I think he does a better job with explaining the basics. I also based my script on his article. In my script I use Promises and the postMessage API.

Setup Service Worker in my 'normal' JS File

// do nothing in old browsers
if (navigator.serviceWorker) {

    // install service worker
    navigator.serviceWorker.register('/service-worker.js', {
        scope: '/'
    });

    // listen for messages
    navigator.serviceWorker.onmessage = function(event) {
        if(event.data === 'offline'){
            // show message to users, to let them know this page is cached
        }
    };

}

/service-worker.js has to be at the root of your project if you want to control all pages. If you need to put your service worker file somewhere else, can also work with a Service-Worker-Allowed HTTP Header. Note that relative URLs are relative to the domain of your document, not your JS file.

The Service Worker itself

Apart from what I described in the section What my Service Worker does this script contains some setup code - mainly setting up eventListeners.

My following Service Worker script can also be found on this gist

var cacheVersion = 'v1';

// Store core files in a cache (including a page to display when offline)
function updateStaticCache() {
    return caches.open(cacheVersion)
        .then(function (cache) {
            return cache.addAll([
                '/offline',
                'https://mycdn.com/style.css',
                'https://mycdn.com/script.js',
                'https://mycdn.com/img/logo.svg',
                'https://mycdn.com/img/offline.png'
            ]);
        });
}

self.addEventListener('install', function (event) {
    event.waitUntil(updateStaticCache());
});

self.addEventListener('activate', function (event) {
    event.waitUntil(
        caches.keys()
            .then(function (keys) {
                // Remove caches whose name is no longer valid
                return Promise.all(keys
                    .filter(function (key) {
                        return key.indexOf(cacheVersion) !== 0;
                    })
                    .map(function (key) {
                        return caches.delete(key);
                    })
                );
            })
    );
});

self.addEventListener('fetch', function (event) {
    var request = event.request;
    // Always fetch non-GET requests from the network
    if (request.method !== 'GET') {
        event.respondWith(
            fetch(request, { credentials: 'include' })
        );
        return;
    }

    // For HTML requests, try the network first, fall back to the cache, finally the offline page
    if (request.headers.get('Accept').indexOf('text/html') !== -1) {
        event.respondWith(
            fetch(request, { credentials: 'include' })
                .then(function (response) {
                    // Stash a copy of this page in the cache
                    var copy = response.clone();
                    caches.open(cacheVersion)
                        .then(function (cache) {
                            cache.put(request, copy);
                        });
                    return response;
                })
                .catch(function () {
                    return caches.match(request)
                        .then(function (response) {
                            if (self.clients) {
                                clients.matchAll()
                                    .then(function(clients) {
                                        // wait until fallback site is loaded
                                        setTimeout(function(){
                                            for (var client of clients) {
                                                client.postMessage('offline');
                                            }
                                        }, 500);
                                    });
                            }
                            return response || caches.match('/offline');
                        })
                })
        );
        return;
    }

    // For non-HTML requests, look in the cache first, fall back to the network
    event.respondWith(
        caches.match(request)
            .then(function (response) {
                return response || fetch(request)
                        .catch(function () {
                            // If the request is for an image, show an offline placeholder
                            if (request.headers.get('Accept').indexOf('image') !== -1) {
                                return caches.match('https://mycdn.com/img/offline.png');
                            }
                        });
            })
    );
});

Jeremy Keith wrote this nice article My first Service Worker, which helped me kickstart my own first Service Worker. I also mixed in stuff from Jake Archibalds postMessage demo to achieve some nice "you are offline" notifications on cached pages.


Tips & Tools

Some Links that will help you if you want to start working with Service Workers:

The basics

A really good starting point for understanding Service Workers: "Introduction to Service Worker" by Matt Gaunt on HTML5 ROCKS

Debugging

Inspecting Service Workers in Chrome is possible here:

chrome://inspect/#service-workers

Service Worker Libraries by Google

You might want to check out a new toolset for working with Service Workers, which Google released recently. (At the time I started writing this article it was not yet released)

Blog Logo

Christoph Rohrer


Published

Image

Collecting Experience

Back to Overview