Service Workers

Why not?

Browser support, that’s the only reason I can think of not to use this. Although, as long as we use the conditional (if ('serviceWorker' in navigator) {) older browsers will still work as normal and the people on modern browsers will get all the offline/performance enhancements that we can achieve. So why not? There is no reason why not! To see who’s going to have access we can keep track of the support with ‘Can I Use’ (as usual) or ‘Is ServiceWorker Ready’. With that out the way, lets look at how we use them.


 


The basics A minimal Service Worker

What actually is a Service Worker and where do I start? It’s a type of web worker, essentially a script file you place at your site’s origin. This is where all our magic will go, but first we have to tell the browser it’s there by registering it from within our ‘normal’ JavaScript.

Somewhere on our home page:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('sw.js', {
        scope: './'
    }).then(function(sw) {
        console.log("Registered!", sw);
    }).catch(function(err) {
        console.log("Error", err);
    });
}

Note, the scope option takes a relative url – if defined, the Service Worker will only run on that url and it’s children.

When your browser finds this registration, it will install our SW in the background but it will not run it, why? Well, we’ve already loaded all the resources, and the page is up and running (if you do want to run it straight away, we have event.replace(); you’ll see where that can go in the next section). Now, if we close out the page (completely, leave no tabs with it open) and then re-open it, the SW will run and do all its magic. After that first load, the browser will always run the SW it has installed. What’s more – it will also check to see if we have made any updates to the SW. If there are changes, the new updated worker will be installed in the background – again, it will not run at this point, it will run on the next load and forever after that until another update happens, exactly like the first time! This is a constantly repeated lesson: for both new and updated workers – first load: it installs, second load: it runs.

Ok, back to the example! With that registration into our page, the browser is going to look for ‘sw.js’ and run it within the context of a worker. This means it runs within a different global context from our page. Trying to get the global scope with window will return an error, instead we have to call self. Also, it doesn’t have access to the DOM, syncronus XHR, or LocalStorage – but it can use quite a few other things. Finally, it’s not tied to any particular page, just the origin (and the scope we optionally defined during registration). We don’t actually have to put anything in this file but that would be pointless – so here’s an example of what we might write.

Inside sw.js – a basic Service Worker script:

self.addEventListener('install', function(event) {
    console.log("First load, now the SW is installed!");
});

self.addEventListener('activate', function(event) {
    console.log("Second load only, the SW activates!");
});

self.addEventListener('fetch', function(event) {
    console.log("a request/response network event is happening:");
    event.respondWith(new Response("SW says hello!"));
});

That’s it! We have a service Worker running! Now when we reload the page it will show “SW says hello!” and will continue to do so unless we do something about it. This is a good place to point out that Service Workers will only run on HTTPS connections. This stops them from being injected somewhere along the network between client and server – just imagine what could be done if a malicious third party managed to register their own SW on our site!


The Service Worker’s Tools

We now have one running but it’s being less than useless. We can catch and hijack network events, but responding with a string is a little limiting, and to do anything more we’ll need a few extra tools: Cache (for storing our things), CacheStorage (for storing our caches – yep, we can have multiple!), and Fetch for fetching our things. I’m really excited about Fetch, it takes XHR and hides the gory details, at last! post-writing note, fetch() is now available in chrome 42!

Cache where we store our resources

The Cache provides storage for request/response object pairs and an API for interacting with it which, currently, is only available to the Service Worker (unless you’re on chrome). It’s where we keep resources for the times when the network isn’t available or too slow. This is a new development that goes alongside Service Workers. As of writing (Q2 2015) it requires a polyfill, but do check if it’s still needed as this technology is still being worked on – probably as you are reading this!

cache.match(request,{options}): see if the request matches anything we’ve cached, returns the request object.
cache.matchAll(request,{options}): same as above but returns an array.
cache.add(request): bit of a combo, fetches a request then puts it into the cache, returns void.
cache.addAll( 'url', 'url', ...): similar to above, but it takes an array, returns void.
cache.put(url, response): adds req/res to the cache (only on GET requests), returns void.
cache.delete(request,{options}): deletes any cache entries that match the request. The options are to further define the way the matching is performed. It returns true if deleted, false if not found.
cache.keys(request,{options}): returns an array of cache keys – it tells us what’s inside it!

It’s also worth noting that browsers have a hard limit on cache size and, in attempting to preserve space, may delete the whole thing. So, we need to try and stay clear of selection for this fate by making sure we delete any redundant objects and (if you still have any hanging around) any old versions of our cache. There is an effort to create a standard that will tell the browser that our cache is really important and please do not delete it, but it’s only in the earliest stages of discussion – just know that it’s being talked about, and hope!


CacheStorage A box that keeps all the caches

As we can store many different caches that can be accessed by Service Workers we need a place where they are all kept: the CacheStorage! It has its own API that we can use, similar to the Cache API but a level up:

caches.keys().then(function(keyList): returns an array of names of the caches stored.
caches.has(cacheName): returns true if the named cache exists.
caches.open(cacheName): returns the named cache object.
caches.delete(cacheName): reletes the named cache object, returns true if deleted, false if not found.
caches.match(request,{options}): same as the match method from the Cache API, except this one checks all Caches in the CacheStorage.

Typically, so far, this is used to separate different versions of our cache. The first SW will create ‘cacheV1’, then the second SW will load in its own ‘cacheV2’ on install and delete ‘cacheV1’ on activate. Don’t delete the previous cache on install – the previous SW may still be using it! We’re safe to clear out old caches on activate as this guarantees that the old SW is no longer running, but I’m getting ahead of myself!


Fetch XHR Revamped

This will be available within both Service Workers and Window objects, check Can I Use for progress. It will allow us to make asyncronous requests with only one requirement – the path to our resource. Optionally, we can give it a parameters object as a second argument to define things like method, our own headers, the body, etc.

It will then return a promise that resolves to a Stream object response that has a few properties for handling: headers, type, status, etc, and the body. There’s a great introduction to fetch article on HTML5Rocks, but (as with everything here) keep looking around for new developments!

A rough example:

fetch('/url', {
    method: 'post',
    headers: {
        'Expires': 'Sat, 28 Nov 2020 05:36:25 GMT',
        'Content-Type': 'application/json'
        ...
    }
}).then(function(response) {
    console.log(response.status);
    console.log(response.type);
    ...
}).then(function(response) {
    return response;
});

A more realistic Service Worker

Those are the goodies we get to play with, so here I’ll run through the three events (install, activate, and fetch) to show how we might typically use those tools within the context of a Service Worker.


Install open a cache, add some resources.

As the install event happens when the browser first encounters a new Service Worker (or an updated one) the code that we run as a result doesn’t impede the user – so we can do a bit of heavy lifting. This, much like the download step of a native app, is where we get everything ready for the offline experience.

self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open('cacheV1').then(function(cache) {
            return cache.addAll(
                '/index.html',
                '/styles/main.css',
                '/scripts/main.js',
                '/images/logo.png',
                '/fonts/icon-font.woff'
            );
        })
    );
});
  • event.waitUntil means the SW will only install successfully once the code passed to it has completed.
  • caches.open('cacheV1') creates a new cache with the supplied name and returns a promise.
  • cache.addAll takes an array of origin relative urls for the resources we wish to store.

A few things to note here:

  • If this is an updated SW, (again) don’t clear the old cache – the current (old) SW may still be using it.
  • The more files we list to be cached, the higher the chance one of them will fail and stop our service worker from being installed.
  • We can also use IndexedDB instead of Cache if you’re not willing to wait for Cache support to catch up, or a polyfill.

Activate tidying up the cache.

This is where we can get rid of any old caches as our new SW has taken control and the old one is now in a redundant state, meaning we won’t break it.

self.addEventListener('activate', function(event) {
    var cacheWhitelist = ['cacheV2'];

    event.waitUntil(
        caches.keys().then(function(keyList) {
            return Promise.all(keyList.map(function(key) {
                if (cacheWhitelist.indexOf(key) === -1) {
                    return caches.delete(keyList[i]);
                }
            });
        })
    );
});
  • cacheWhitelist an array of the names of caches we do not want to clear out.
  • waitUntil() blocks any further requests until it has completed, so we won’t have to deal with any until our activation tasks have completed.
  • caches.keys() gives us the names of all the caches (keyList) in the cacheStorage.
  • Promise.all will only resolve when caches.keys() has completed.
  • keyList.map(function(key)) we’re going to iterate over every element in the array running this function to which we pass each keyList item to be checked against our whitelist; if the current cache (key) isn’t named in the white list, it gets deleted.

Thanks to Mozilla for that example!


Fetch event the main work of the Service Worker

We’ve already seen the basic implementation of this above, (inside the fetch event handler: event.respondWith(new Response("SW says hello!"));). It’s how we hijack any network requests in order to perform our magic on them (which usually ends up with event.respondWith(...)). Unlike the install and activate events above, the fetch event has to handle quite a few different scenarios. Fortunately, Jake Archibaled has put together a short cookbook of patterns to deal with many of them. Below I’ll explain one of them:

//Check if in cache, if not, get from network - add to cache and return.
self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches.open('cacheVn').then(function(cache) {
            return cache.match(event.request).then(function (response) {
                return response || fetch(event.request).then(function(response) {
                    cache.put(event.request, response.clone());
                    return response;
                });
            });
        })
    );
});
  • caches.open('cacheVn') opens the current (named) cache and passes it forward as ‘cache’.
  • cache.match(event.request) checks the cache for any matching keys (the key is the request address), passes forward the object if there is one.
  • fetch(event.request) will only fire if there is no response object, this is how we go to the network!
  • cache.put stores the resource, we pass the url and response.clone() – response can only be used once, which is why we have the clone() method.
  • Finally we return the response!

Debugging Service Workers

Because they don’t run within the context of our page, they don’t show up within our DevTools panel. Instead the Chrome team have given us a couple of different ways to dig into Service Workers from the browser:

chrome://inspect/#service-workers lists any Service Workers we have registered in our browser. Terminate to terminate, inspect to open a DevTools window that’s focused on the SW. Now we can play around with it to our hearts content! Note, not all the DevTools panels are useful (eg elements) as the SW doesn’t have a DOM.

chrome://serviceworker-internals will also show a list of service workers that have been registered. For each we’re given some details, a console and a few buttons to play with. I believe these will be changing in the near future so I won’t go through them, but they seem simple enough for now! The only one to note is inspect as that will open DevTools in the same way as described above.

Finally, from within our page’s code we can find out which SW is in control by calling navigator.serviceWorker.controller. If there isn’t an SW, that will return null.


Conclusion

I’ve been hearing about Service Workers for a while but they always seemed to be something for the future. Now that I’ve dug into them and had a detailed look around, I’m convinced that they’re together enough for us to start using them! We can even apply them to existing sites – all they need is that registration code inserted, then away we go to the separate js file to manage the magic. And, while we’re working on perfecting the client’s interaction with the network, there will be a few more things that we can look forward to incorporating with SW:

  • Background sync: check over the network for content updates – doesn’t require the user to have the site open.
  • Push: send the user alerts (possibly when new content is available) – again, doesn’t require the site to be open.
  • Geo-fencing: triggering some functionality based on location.
  • Time/date alerts: while the site isn’t open.

For further reading I’d definitely recommend going through the examples in the cookbook mentioned before, and if you’re up for some dryer reading: The spec. Finally, now that you’ve read through all those things and are a Service Worker pro, go check out what’s happening on Stack Overflow!

« Prev Article
Next Article »