Getting Started with Service Workers

Ritesh Kumar
Share

There was a time when people only related the use of push notifications to mobile applications. Luckily, that time has passed. Now there are Service Workers which can help us implement push notifications in desktop applications, and open up websites even when you’re offline.

A Service Worker is a script that runs in the background. It doesn’t need a web page or user interaction in order to work. This means that it will run even when your website is not open, even if it can’t access the DOM directly (the DOM can use the postMessage API to communicate with the Service Worker, though). Currently, they include features like push notifications and geofencing. It can also intercept and handle network requests, that is the feature that we’re going to use in this tutorial. To those of you who are curious about browsers support, I suggest to take a look here. As you’ll see, its implementation is still at an early stage.
In order to demonstrate how the network intercepting feature of Service Workers works, we’ll make a static website which runs even when the user is offline. You can find the whole demo of the website here.

Service Workers give you the control of a web page where you can programmatically select the components you want to cache. Keep in mind that it’ll run offline only on second or subsequent visits. The reason behind this behavior will be explained later in this tutorial.

final result

One common problem that Service Workers have is that they only work in “secure origins” (HTTPS sites, basically) in line with a policy which prefers secure origins for powerful new features. However, even localhost is considered a secure origin, so developing on it is an easy way to avoid this error. If you prefer, you could also use GitHub Pages (as I did) since they’re served over HTTPs.

Getting Started

The first thing that we need to do is to register the Service Worker. This will work only if the browser supports it. This means that all the following code snippets you’ll find throughout this tutorial will be valid only if navigator.serviceWorker exists.

//make sure that Service Workers are supported.
if (navigator.serviceWorker) {
    navigator.serviceWorker.register('./service-worker.js', {scope: './about'})
        .then(function (registration) {
            console.log(registration);
        })
        .catch(function (e) {
            console.error(e);
        })
} else {
    console.log('Service Worker is not supported in this browser.');
}

In the above code ./service-worker.js is the path of the Service Worker. The scope is the path on which the Service Worker will act. In this example the Service Worker will control the page having the path /about/. The scope is optional and has ./ by default. The register method returns a promise. We can call the register method as many times as we want. When this is done, the browser will automatically figure out if it has been already registered and will register it only if it hadn’t been registered earlier.

You can view all the registered Service Workers by going to chrome://serviceworker-internals.

Installation

In a Service Worker we can register event listeners for various events triggered by the browser. The install event is triggered when the browser sees the Service Worker for the first time. When you open the Chrome’s developers tools you won’t be able to see the log because the Service Worker runs in a completely different thread. We’ll discuss more on debugging in the later part of the tutorial.

self.addEventListener('install', function(event){
	console.log(event);
});

self.addEventListener('activate', function(event){
    console.log(event);
});

At this point we’ll intercept the requests made to the server. To do so, we listen to the 'fetch' event using the self.addEventListener method, which returns the event object in the callback. We get the request URL as the value of event.request.url.

self.addEventListener('fetch', function(event){
  console.log(event.request.url);
  // return something for each interception
});

If you want to import any external script in the Service Worker, you can do it using importScripts() . In this example we’ll be using the cache-polyfill since the support for cache is limited.

importScripts('js/cache-polyfill.js');

var CACHE_VERSION = 'app-v1';
var CACHE_FILES = [
    '/',
    'images/background.jpeg',
    'js/app.js',
    'css/styles.css',
    'https://fonts.googleapis.com/css?family=Roboto:100'
];

self.addEventListener('install', function (event) {
    event.waitUntil(
        caches.open(CACHE_VERSION)
            .then(function (cache) {
                console.log('Opened cache');
                return cache.addAll(CACHE_FILES);
            })
    );
});

In our install event listener, we use the waitUntil() method from the provided event object to tell the browser with a promise when the installation process in our Service Worker is finished. The provided promise is the return value of the caches.open() method that opens the cache with name ‘app-v1’.

Once the cache opens properly we add our assets to it. The install method only finishes once the assets are saved. If there is an error in saving any one of the assets, then the Service Worker won’t be registered successfully. This means that we should ensure that we only cache important files as a higher number of files will increase the probability of failure. You should only cache the components that improve the perceived load time of the web page.

When the installation step is finished, the Service Worker activates. This is where the Service Worker takes control of the page.

Now the requests are being intercepted but we need to figure out what we are going to do once this happens. There may be cases when the Service Worker can’t read the data from the cache or the request doesn’t match the assets’ request URL that is saved in the cache.
Here’s what we’re going to do once we intercept the request:

  1. First we open the cache and match the request with the ones present in the cache. If they match, we return the data from the cache. If the request doesn’t match, we redirect the request to the server.
  2. When the data is received successfully from the server, we return that data.
  3. Then we open the cache and save that data here using cache.put() so that it can be accessed directly from the cache in following attempts.
self.addEventListener('fetch', function (event) {
    event.respondWith(
        caches.match(event.request).then(function(res){
            if(res){
                return res;
            }
            requestBackend(event);
        })
    )
});

function requestBackend(event){
    var url = event.request.clone();
    return fetch(url).then(function(res){
        //if not a valid response send the error
        if(!res || res.status !== 200 || res.type !== 'basic'){
            return res;
        }

        var response = res.clone();

        caches.open(CACHE_VERSION).then(function(cache){
            cache.put(event.request, response);
        });

        return res;
    })
}

Now, let’s analyze a scenario in which we would need to update the cache, which is common since it’s necessary every time the files are changed. Once your files have been changed, you need an update in the cache. Here is how we have to proceed:

  1. Update CACHE_VERSION because if the browser detects any change in the Service Worker, it will redownload it. The install event in the new service worker will be fired but the new Service Worker will enter in the ‘waiting’ stage as the page will still be controlled by the old Service Worker.
  2. When all the instances of your website are closed, the new Service Worker will take control (instead of the older one).
  3. At this point the install event will be fired and here we’ll need to do some cache management.

We’ll find all the keys different from the current version and then we’ll clean them by using the function below.

self.addEventListener('activate', function (event) {
    event.waitUntil(
        caches.keys().then(function(keys){
            return Promise.all(keys.map(function(key, i){
                if(key !== CACHE_VERSION){
                    return caches.delete(keys[i]);
                }
            }))
        })
    )
});

The ServiceWorkers will be installed when you visit the website for the first time. Don’t expect them to take control of the page on the first visit. They will just get registered and installed. The request will go to the server and the assets will be fetched from there. Moreover, in the meantime they’ll be saved in the cache. In later visits, the Service Worker will intercept the requests and return the assets from the cache.

In order to get a better feel of all this, open the Networks tab in the developer tools. If you re-open the page later, you’ll see that all the cached assets are being fetched from the Service Worker.

Network

One thing to keep in mind is that the browser controls the lifetime of a Service Worker. The time it runs for after the installation is not fixed.

Debugging

Service Worker’s debugging is a bit tricky for a beginner. You have to enable it as it’s still an experiment. To do it, follow these steps:

  1. Go to chrome://flags and enable the option ‘Enable DevTools Experiments’.
  2. Open DevTools, then go to Settings > Experiments and hit Shift 6 times.
  3. Check “Service Workers in Resources panel” and restart DevTools

Now you have this experiment enabled and you can find the option in the Resources tab of DevTools.

Debugging

If you want to manually unregister a Service Worker, go to chrome://serviceworker-internals/ and click on the corresponding “Unregister” button. Some more insights into the debugging process can be found here.

Conclusions

In this article we have created a website which demonstrates the use of Service Workers to create offline web applications. We also discussed few concepts regarding Service Workers and how to debug them.
I really hope you enjoyed the tutorial.
If you want to play with the source code, you can find it here.