Before you expend energy creating a service worker script, you might be wondering if it’s worth the investment. You probably want to know which browsers support service workers, and by extension, how many of your site’s visitors will benefit from this technology.
You can go to caniuse.com and find the current support levels for service workers (Fig 2.1). At the time of writing, it’s not exactly a field of green. Quite a few of the major browsers support service workers, but there are some glaring omissions. Some of the visitors to your website are almost certainly using browsers that don’t support service workers.
You could wait until just about every browser ships support for service workers before adding this technology to your site. Though, fortunately, because of the way service workers have been designed, you don’t have to wait. You can deploy a service worker to your site today. The supporting browsers will get the benefit, and the non-supporting browsers will carry on just as they do right now. Think of your service worker as a reward for users of more modern browsers. Crucially, you won’t be punishing users of less modern browsers.
Before a service worker can be installed on a user’s machine, they must first visit your website. For that first visit, there’s no service worker, regardless of whether the user’s browser has support for service workers or not. When it comes to first-time visits, no browser can benefit from a service worker. That means a service worker can only be deployed as an enhancement. Even if you wanted to make a website that relied completely on a service worker, that first visit would foil your fiendish plan.
I think that’s a brilliant piece of design. Because service workers must be applied as an extra layer on top of your existing functionality, the levels of support on caniuse.com really don’t matter. Even if only one browser supported service workers, it would still be worth adding one to your site. Best of all, as more and more browsers add support for service workers, more and more people will benefit from the work you do today.
Start by creating a blank JavaScript file called serviceworker.js and save it in the same folder as your website. You might be used to putting all your JavaScript files into their own folder, like /js/, but I recommend keeping your service worker file at the root level. If you put it somewhere else, things get complicated when it comes to which URLs the service worker can intercept. Putting your service worker file at /serviceworker.js keeps things simple.
Before the service worker can be installed on a visitor’s machine, the visitor’s browser needs to know of the file’s existence. You need to point to the service worker file and say, “See that service worker script over there? Install it, please.” This is called registration.
The simplest way to do this is with a link element in the head of your site’s HTML:
<link rel="serviceworker" href="/serviceworker.js">
Alas, we can’t rely on this just yet. At the time of writing, not many browsers support this nice declarative way of pointing to service worker scripts. But that’s okay. We can still use JavaScript. You can put this JavaScript in an external file or put it at the bottom of your HTML:
<script>
navigator.serviceWorker.register('/serviceworker.js');
</script>
This highlights an interesting difference between HTML and JavaScript. If a browser doesn’t support service workers, and you present it with the link rel="serviceworker" element, the browser will ignore it. That’s down to the error-handling model of HTML—it ignores what it doesn’t understand. That can be frustrating if you’re trying to debug HTML. If you make a typo, the browser won’t complain—it will simply ignore it. But it’s a powerful feature when it comes to extending the language. New elements, attributes, and rel values can be added to HTML, safe in the knowledge that older browsers will quietly ignore them and move on.
That’s not how browsers behave with JavaScript. If you give a browser some JavaScript it doesn’t understand, it will throw an error. Worse, the browser will stop parsing that block of JavaScript. Any subsequent code, even if it’s error-free, will never get executed.
If you point a browser at a service worker script using JavaScript, but that browser doesn’t understand what you mean by navigator.serviceWorker, it won’t just ignore what you’ve written—it will throw an error.
There’s a way around this. Before using a browser feature in JavaScript, you can ask the browser whether or not the feature exists. This is imaginatively called feature detection.
You can apply feature detection to just about anything that’s available through JavaScript. If you wanted to use the Geolocation API, your feature detection might look like this:
if (navigator.geolocation) {
// Your code goes here.
}
That line beginning with // is a comment. It won’t be executed by the browser. It’s not meant for machines; it’s meant for humans. Comments are a great way of leaving reminders for your future self, like Guy Pearce in Memento or Arnold Schwarzenegger in Total Recall.
The comment is there for you. The if statement is there for the browser. The if statement checks to see if there’s such a thing as a geolocation property in the navigator object.
Wait a minute. Objects? Properties? What is this moon language I’m suddenly spouting?
For the longest time, I was intimidated by concepts like Object-Oriented Programming. Not only was it written in capital letters to demonstrate its seriousness, it also had its own vocabulary of terms. I knew some JavaScript, so I knew what a variable was (a label for storing a value—the value can change, but the label stays the same), and I knew what a function was (a block of code that can be executed by invoking its label), but I had no idea what a property or a method was.
Imagine my surprise when I found out that a property is just another name for a variable, and a method is just another name for a function. The only difference is that properties and methods have a parent, called an object—not exactly a very revealing name (we’re lucky we didn’t up with Thing-Oriented Programming).
Properties and methods are preceded by the parent’s label and a dot:
object.property
object.method()
Web browsers expose their features to JavaScript through objects. There’s one parent object called window. That object contains other objects, chained together with dots. The document object belongs to the window object:
window.document
So does the navigator object:
window.navigator
So document and navigator are properties of the window object, as well as being objects themselves.
The window object is so ubiquitous that you don’t even have to specify it if you don’t want to. That’s handy because document and navigator can have their own objects, which in turn have their own properties and methods. It can get quite long-winded to write:
window.navigator.serviceWorker.register();
You can save a bit of space by writing:
navigator.serviceWorker.register();
That’s the register method of the serviceWorker object: the serviceWorker object is a property of the navigator object (which is in turn a property of the window object). You can read it backwards from right to left, substituting each dot for the words “belongs to”: register belongs to serviceWorker, which belongs to navigator (which belongs to window).
With feature detection, you’re checking for the existence of properties. If you want to use service workers, you can first ask the browser if there’s a property called serviceWorker that belongs to the navigator object:
if (navigator.serviceWorker) {
// Your code goes here.
}
In an older browser that doesn’t support service workers, navigator.serviceWorker returns a value of undefined—there’s no such property. In a newer browser, navigator.serviceWorker exists and is an object.
There are many ways to do feature detection. You could use the in operator to rifle through the navigator object looking for the serviceWorker property:
if ('serviceWorker' in navigator) {
// Your code goes here.
}
Or you could explicitly check that the serviceWorker object doesn’t have a value of undefined:
if (navigator.serviceWorker !== undefined) {
// Your code goes here.
}
Whichever way you decide to apply feature detection, it’s always a good idea to do it before using a browser feature. With this in mind, here’s how you can point to your service worker file from your HTML:
<script>
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/serviceworker.js');
}
</script>
Now you’re safely running the register method of the serviceWorker object, secure in the knowledge that nonsupporting browsers will never try to execute that code.
Whenever I see that something is a method, I do a little mental substitution—replacing the word method with the word function—to remind myself how methods work. The parentheses after the name of the method are a dead giveaway that methods work just like functions (they just happen to be functions that belong to a parent object).
The parentheses are where we can pass in values to a function—or to a method. For some reason, these values are known as arguments. I have no idea why this is. It makes talking about code sound quite confrontational: “Pass these arguments into this method” sounds like an instruction to pick a fight.
In the case of the register method, you’re currently passing in one argument—the URL of your service worker script:
register(url)
The value of that URL will define the scope of the service worker—how much of your site the service worker will control.
By default, the scope is derived from where you put your service worker script. If your service worker script resides at /js/serviceworker.js, the script will only be able to control URLs that start with /js/.
There might be situations when you want the same domain to have multiple service workers, such as /myapp/serviceworker1.js and /myotherapp/serviceworker2.js. Because the scope of a service worker is defined by its URL, you can point to both of them from anywhere in your site:
navigator.serviceWorker.register('/myapp/serviceworker1.js');
navigator.serviceWorker.register('/myotherapp/serviceworker2.js');
The first service worker will have control over /myapp/. The second service worker will have control over /myotherapp/.
What if you have one service worker for the whole site, but another one for a specific folder?
navigator.serviceWorker.register('/serviceworker1.js');
navigator.serviceWorker.register('/myapp/serviceworker2.js');
First you’re declaring that one service worker should have control over every URL, then you’re declaring that another service worker should have control over certain URLs. Which declaration wins?
There’s a fairly simple formula for figuring that out: the service worker script with the longest path in its URL will win. The service worker inside myapp will handle any requests that start with /myapp/. Every other URL will be handled by /serviceworker1.js.
Another option is to put all your service worker scripts at the root level, and then declare the scope from JavaScript. Let’s say your scripts are /serviceworker1.js and /serviceworker2.js. The first service worker script is for the whole site, so you can point to it like this:
navigator.serviceWorker.register('/serviceworker1.js');
The other service worker script is only intended for /myapp/. You can declare this by passing in another argument to the register method:
navigator.serviceWorker.register('/serviceworker2.js', {
scope: '/myapp/'
});
The declarative equivalent of this—once browsers support it—will be:
<link rel="serviceworker" href="/serviceworker1.js">
<link rel="serviceworker" href="/serviceworker2.js" scope="/myapp/">
While it’s good to know how to set the scope of different service workers, most websites will only ever have one service worker responsible for the whole site.
The register method lives up to its name. You’re asking the browser to register the existence of a service worker script. Your code should look something like this:
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/serviceworker.js');
}
When you ask the browser to register the existence of your service worker script, you’re going to have to give it a few moments. First, the browser needs to verify that the current site is either running on HTTPS or localhost. Then, it needs to check that the service worker script is on the same domain as the current site. Finally, the browser will attempt to fetch the service worker script and parse it.
None of these steps will take very long, but you wouldn’t want the browser to freeze while it’s busy with these tasks. That’s why the register method is executed asynchronously. The browser doesn’t finish executing the register method before moving on to the next line of code. Instead, it moves straight on to the next line of code while it carries out its tasks in the background.
That’s great for browser performance, but what if we want to give the browser some further instructions once the register method has finished its chores?
The old way of executing the extra instructions would involve listening out for events—maybe something like load or ready. That works, but it can result in code that’s hard to read. There’s another way of handling asynchronous events that results in more elegant code: promises.
A promise is a kind of object that comes with a built-in method called then. Whatever function you put inside the then method will only be executed when the promise has successfully finished all its tasks. At this point, we say that the promise has been fulfilled, much like the closing chapter of a revenge thriller or the denouement to a fairy tale.
promise
.then( function () {
// Yay! It worked.
});
If something goes wrong along the way and the promise isn’t fulfilled, there’s a corresponding catch method. You can put a function in there to make amends for the unsuccessful fulfillment of the promise. The end result looks something like this:
promise
.then( function () {
// Yay! It worked.
})
.catch( function () {
// Boo! It failed.
});
You don’t have to put the then and catch methods on new lines like that. That’s just my preference. You might prefer to write:
promise.then(
function () {
// Yay! It worked.
}
).catch(
function () {
// Boo! It failed.
}
);
You could even write the whole thing on one line if you want to be the James Joyce of JavaScript.
In those examples, the functions inside then and catch are anonymous functions. That doesn't mean that they’re ashamed of anything they're doing; it means that they don't have names. They’re created on the fly and then never referred to again. You don’t have to use anonymous functions. You could invoke functions that you’ve written elsewhere—functions that are proud of their names, not hiding behind the veil of anonymity:
promise
.then(doSomething)
.catch(doSomethingElse);
If I were the judgmental sort, I would have to say that doSomething and doSomethingElse aren’t names to be proud of, but the point is they can be reused. And if you use them within then or catch you know that they won’t run until the promise is fulfilled or rejected. That’s right—we call it a rejection when a promise isn’t fulfilled.
Promises, fulfillments, and rejections—this is beginning to feel like a soap opera.
Promises are perfect for asynchronous tasks. Registering a service worker is an asynchronous task. Let’s prove it. Try out this piece of code:
<script>
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/serviceworker.js')
.then( function () {
console.log('Success!');
})
.catch( function () {
console.error('Failure!');
});
console.log('All done.');
}
</script>
Add that JavaScript to the bottom of your HTML page, reload the page in a web browser, then open up the browser’s JavaScript console (alt+cmd+j). Here’s what you should see:
All done.
Success!
Unless something went horribly wrong, in which case you’ll see:
All done.
Failure!
Notice that the command to log “All done.” was at the end of your code, and yet it’s the first log command to get executed. Usually JavaScript code is executed in a procedural way—the order in which commands are given is also the order in which those commands are executed. Asynchronous commands—like serviceWorker.register—will finish executing in their own good time. That’s asynchronousness… asynchronicity… asynchronaciousness… that’s how this kind of thing works.
When a promise is fulfilled (or rejected), it can send data to the function that’s waiting patiently inside then (or catch). To access that data, you’ll need to include it as an argument inside the waiting function. Here’s an example:
navigator.serviceWorker.register('/serviceworker.js')
.then( function (registration) {
console.log('success!', registration.scope);
});
In this case, I’m passing the data from successful registration in a variable called registration. That data is an object. I’m then accessing the scope property of that object. That gives me something like:
Success! <http://localhost:8000/>
That word registration is just what I’m calling the object being returned from a successful service worker registration. I could call it anything—for instance, this code works exactly the same way:
navigator.serviceWorker.register('/serviceworker.js')
.then( function (x) {
console.log('success!', x.scope);
});
Whenever you receive data from a promise, you can call it anything you want. Personally, I think that registration makes more sense than x because it describes the data better.
A promise can also pass data to the function within catch. That’s really useful for debugging. Here’s an example where I’m deliberately going to cause an error by trying to point to a non-existent service worker file:
navigator.serviceWorker.register('/nothing.js')
.catch( function (error) {
console.error('Failure!', error);
});
Now I’ll see something like this in the console:
Failure! TypeError: Failed to register a ServiceWorker: A bad HTTP response code (404) was received when fetching the script.
Again, that name error is just my name for the data. I could’ve called it x or y or anything:
navigator.serviceWorker.register('/nothing.js')
.catch( function (y) {
console.error('Failure!', y);
});
Feel free to update the JavaScript code in your HTML to take advantage of the data being passed in from the register promise:
<script>
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/serviceworker.js')
.then( function (registration) {
console.log('Success!', registration.scope);
})
.catch( function (error) {
console.error('Failure!', error);
});
}
</script>
Looking good. You’re practicing feature detection, you’re handling promises, and most important, you’re registering a service worker for your site. But that service worker isn’t doing anything yet. It’s just a blank file.
Let’s fix that.
Making Fetch Happen