Are We Online Yet? - Beyond Responsive: Optimizing For Offline - Responsive Web Design, Part 2 (2015)

Responsive Web Design, Part 2 (2015)

Beyond Responsive: Optimizing For Offline

BY JOHN ALLSOPP AND MATT GAUNT

We spend a lot of time discussing layouts and breakpoints and media queries and adjustments between various form factors, but perhaps there is one aspect of responsiveness that we tend to not spend enough time talking about. What if we think a bit beyond all those familiar aspects, and consider how responsive a website or an application should be when the user isn’t online? What if I told you that as a user, you don’t have to be online to use the web, and a website or a web application would respond to this accordingly?

There’s a general (and understandable) belief held by many developers, not to mention most users, that websites and web applications have a very serious limitation — they can only be used when the browser has a web connection. Indeed, this is routinely cited as one of the real advantages of so-called native apps over the web. Typically, when a user was offline, if they tried visiting a URL, even one they’d recently visited, the browser just wouldn’t load the page. The rise of mobile, portable computing didn’t make the situation easier: these days users are less likely to have guaranteed high-bandwidth connectivity and more likely to be connected via slower cellular networks. Because users are on the go, often being online isn’t even a matter of good coverage, but rather a matter of difficult locations which don’t have a reliable connectivity — trains, tunnels, remote locations, disconnected environments.

As counter-intuitive as it sounds, in almost every modern browser on any device (including Internet Explorer from version 10), it’s no longer the case that users need to be connected to the web to use our websites and applications, provided we do a little extra work to make our site or application persist when a browser is offline.

This opens up a whole range of opportunities, levelling the field with native apps that can be installed on the user’s phone, tablet, laptop or desktop computer, or indeed any other device capable of running apps. But there are many more benefits to offline technologies than simply allowing websites and apps to work offline, as we’ll soon discover.

As the team behind the Hoodie1 framework wrote on their “Initiatives” page2, even in the developed world, mobile bandwidth and connectivity, increasingly the primary way in which people connect to the Web, are not always guaranteed to be there, or be reliable. They state:

“We can’t keep building apps with the desktop mindset of permanent, fast connectivity in mind, where a temporary disconnection or slow service is regarded as a problem and communicated to the user as an error.”

And when we think of offline and online, we typically only focus on the client; but servers go offline as well, for routine maintenance, or in times of crisis, or under heavy stress. What if your user could continue to use all, or the core, of your site’s functionality even when your site is offline?

In this chapter, we’ll cover a few technologies and practices that you’ll need to use to make your apps work as well offline as they do online. We’ll discuss how to detect if we are online or not, HTML5 Application Cache, Web Storage, offline events, and emerging standard such as service workers. Let’s dig into it. This is going to be quite a journey.

Are We Online Yet?

When we develop a website or app that might be used online or offline, it’s useful to know whether the browser is currently connected. We might enable or disable upload or submit buttons as appropriate, or otherwise adapt a user interface or other functionality based on the current connection state.

You might have thought it was straightforward for a browser to know whether it is online or not, and then to let us know. Sadly, this isn’t the case. HTML5 gives us two ways we can try to determine the current online status of the browser. We can check the onLine attribute of the navigator object, or we can listen for online and offline events. We can also use the (currently still in draft) Network Information API standard supported in some browsers. Additionally, there are some other hacks for trying to determine whether the user is connected. We’ll cover them below.

NAVIGATOR.ONLINE

The navigator object is part of the DOM (or more accurately the BOM or browser object model) that represents the browser itself. It’s traditionally most commonly used to access information about the browser version, but increasingly we’re seeing device-level APIs like DeviceMotion associated with the navigator object. One of the properties of the navigator object is onLine, which is true if the browser is online and false if offline.

But navigator.onLine will be false if the browser is definitely not connected to a network. However, it’s true if it is connected to a network, even if that network is not connected to the internet. So, while a value of false indicates we’re definitely offline, a value of true does not necessarily mean we will be able to connect to a web server.

There’s an added complication in Firefox and Internet Explorer. These browsers have an offline mode that allows the user to disconnect the browser from the internet, even while the system they’re running on is connected. In Firefox and Internet Explorer 8+ navigator.onLine means both that the local system is connected to a network (as described above) and the browser is not in this offline mode.

In Internet Explorer 7, a value of false indicated solely that the user was in offline mode while a value of true indicates nothing about whether the system itself was connected to a network.

In summary, the value of navigator.onLine is of limited value. We can use it to determine (in all browsers from Internet Explorer 8+) that the browser is definitely offline, but not to determine that the browser definitely has a connection to the internet. We’ll see shortly that this still has some benefits.

ONLINE AND OFFLINE EVENTS

While it’s good to be able to check whether the user is (probably? possibly?) online or offline, it would be nice to not have to constantly ask the navigator object whether the user is connected or not, but receive a notification when the user goes online or offline. We can do this by providing an event handler for two different events, offline and online. In theory, we can attach this handler to the window, document or even body objects. But in practice, the only way to attach an event handler that works across all modern browsers which support online events is to attach it to the window, using addEventListener (but we can’t use window.online = function reference). So, we’d ask the window to call the function updateUI() when the user goes offline like this:

window.addEventListener("offline", updateUI);

Again, as with navigator.offLine, with WebKit browsers the events are fired when the user connects to, or loses connection to, a local area network (for example by turning off Wi-Fi or unplugging from ethernet) or, with Firefox and Internet Explorer 8+, also when the user goes into or out of offline mode.

How might we use these events or the navigator.onLine property?

A simple way to improve our application or page user experience would be to disable options that require the user to be online (for example, a submit button for a form) and to inform the user somehow why this is disabled.

For operations which happen without user intervention between browser and server (for example, synchronizing data using XMLHttpRequest), rather than attempting the operation and waiting for a timeout, we could determine whether the user is definitely offline or not, and if offline, save the data to synchronize in localStorage, then try the operation once the browser receives an online event.

W3C NETWORK INFORMATION API

Clearly, as web applications become more sophisticated, we’d like to know more than whether the browser might be connected to the web. To this end, the W3C is currently developing the Network Information API, which provides information about the current network bandwidth availability.

NAVIGATOR.CONNECTION

The Network Information API adds a connection object to the navigator. The connection object has two properties:

•bandwidth: a number that represents the current download bandwidth. If the browser is offline, this is 0. If the bandwidth is unknown, the value is infinity.

•metered: is true if the connection is metered (the user is currently paying for their bandwidth).

The connection object also receives a change event when the connection changes. This could be because the user has moved from a metered to a non-metered connection or because the network speed has changed. If a change occurs, we could determine whether the current bandwidth is zero and, if so, we know the browser has now gone offline. For subsequent change events, we could check to see whether this value is no longer zero, in which case the browser will now be online.

Currently, the Network Information API is still a draft specification, though it is supported in Firefox and Android. Why mention it? Well, as we saw, navigator.onLine and the online and offline events only really tell us whether a browser is definitely offline or possibly online.

But because Mozilla-based browsers support a draft version of the Network API, we can use this API to determine whether Firefox is really online.

navigator.connection.addEventListener('change', connectionChanged, false);

//Add the change event listener

function connectionChanged(event){

if(navigator.connection.bandwidth !== 0) {

//We're online

}

else {

//We're offline

}

}

OTHER WAYS OF DETERMINING WHETHER WE’RE OFFLINE OR ONLINE

Because of the shortcomings of onLine and of the offline and online events, as well as lack of widespread support for the Network Information API, several developers, including Remy Sharp and Paul Kinlan, have come up with a number of clever ways to try to detect whether a browser is online or offline. These include using the AppCache errors and XMLHttpRequest. Here’s a quick overview of these techniques.

USING THE APPCACHE ERROR EVENT

As we’ll find out later, if we request an update to the AppCache and something goes wrong, we receive an error. Now it could be that the manifest file or one of the resources in the manifest are missing, but if we know they aren’t, we might reasonably guess that the user, our server or both are offline, which for the purposes of our site is largely the same thing most of the time.

Let’s add an event handler for the error event to the AppCache so that when the AppCache is updated we record the current status.

window.applicationCache.addEventListener(“error”, onlineStatus, false);

function onlineStatus(event) {

//We’re probably offline as we got an AppCache error

}

One downside to this is we’ll only check the online status when the page originally loads, so if the user comes online we’ll have to manually trigger an AppCache update.

function checkOnline() {

//Try to update the appcache. If that throws an error, it will call onlineStatus

window.applicationCache.update();

}

USING XMLHTTPREQUEST

One of the more traditional ways of determining whether a browser is online is to make an XMLHttpRequest (XHR) for a resource on a server. The idea is quite straightforward, though the code involved is somewhat convoluted; you can grab Remy Sharp’s polyfill using XHR3 and Paul Kinlan’s detailed HTML5 Rocks article4 which has a fully working example of the XHR approach (along with the AppCache approach).

Reliably detecting whether a browser is online is still far from straightforward, particularly doing so across several browsers; certainly, there’s no reliable standards-based way. There are, however, techniques you can use that will help you determine this with some confidence, should it be something vital you need to know about.

Service Workers

We can figure out when a user is definitely offline. What now? Well, we can design the experience for the case when online isn’t available. There are a number of ways to do that. We are pretty familiar with the good ol’ HTTP Cache, yet its major limitation is that it wasn’t designed to allow sites to work while a browser wasn’t connected to the network. Service workers were designed to solve just this issue. As a successor of AppCache, service workers resolve quite a number of its shortcomings, and when used properly they can do the job well.

We can’t cover AppCache in full detail in this chapter, but it’s important to understand that with AppCache we basically tell browsers what we want cached and let them work out how to do the caching, routing and updating; this hands-off approach is declarative. The service worker approach requires us to use code to control how and when resources are cached. This means we have to write a little more code, but we can create experiences that are simply impossible with a declarative approach. Service workers, like web manifests, are very much in a state of development. But even more than web manifests, they are supported by multiple browsers (including Chrome and Firefox) and will be the future of how web applications work offline.

WHAT IS A SERVICE WORKER?

The best way to think of a service worker is as a special background process running JavaScript that the browser can treat as a proxy server on a user’s device. A web-based application can register a service worker, which will then intercept all network traffic between your application and the outside world.

Once a service worker is registered and installed for a site (in essence, for a domain or path within a domain, called the scope), the next time the user attempts to navigate to that domain, whether by a bookmark, link, entering a URL in the browser or any other way, the browser’s first step will be to start up the service worker — which will be installed locally and be available offline — and dispatch an event to the service worker for each network request.

For instance, if we have a service worker installed for https://gauntface.com and the browser receives a request for any page on this domain, the request will be sent to the service worker, as will any request made by these pages. The service worker can then do whatever it wants: modify the request to retrieve something different; simply respond with some text without passing that request on to the network; or just leave the request alone and let the browser do what it normally would. The key thing to realize is that for the first time, you can sit between your web page and the internet to decide how to handle network requests.

In case you were wondering how this would affect load time performance, it takes negligible time to start a service worker and handle requests. Once set up, you’ll be caching assets, which prevents a network trip and download so you’ll quickly improve the load time of your page. As for runtime performance, it’s not affected since service workers are a special kind of JavaScript worker, which run in the background on a different thread and have no direct access to the DOM.

WHY ADD A SERVICE WORKER?

There are a few reasons you should consider adding a service worker.

•Caching for an entirely offline experience.

•Improving the speed of your site by caching key resources.

•Ability to have greater control of network requests and failures.

The one use case you’ll hear over and over again is that you can make your web app work offline — and you totally can. If you have a simple single-page web app or static site, offline support is trivial: you just need to know what files to cache in your service worker. If you have a large dynamic site (think e-commerce) then it doesn’t make sense to cache everything. Instead, you should cache certain key pages or assets of your web app.

But there are other use cases and you could argue they are a little easier to stomach than full offline support when you first start using service workers. Many sites will have certain files that won’t change that frequently, particularly JavaScript, CSS or font files, for example. These assets would be prime candidates for caching with a service worker since it will improve the load time performance of your site by skipping the network request. You can then decide when and how to update the cached version of those files.

The great thing with service workers is that they can be treated as a progressive enhancement. Since service workers intercept network requests and run outside of the page, you don’t have to code your site any differently to take advantage of these features. Should there be any kind of problem loading the service worker script, or if the browser doesn’t support service workers at all, then it will fail to register and the browser will simply carry on as normal, making it easy to treat as an enhancement to your app.

Finally, one big concern developers had with AppCache is that if something went wrong, you could get into a scenario where you couldn’t update the cached version of your site on a user’s device: the user would have to know how to clear the cache manually. When the browser requests a service worker script, a Cache-Control header is used to determine how long it should be cached for; but the browser will cap the cache to 24 hours, which means that it will always try to get a new version of the service worker script at least once every 24 hours. This allows you to avoid the scenario where you can’t gain control of your site.

CACHING WITH SERVICE WORKERS

One of the most important capabilities we need for offline web content is the ability to cache resources. Service workers allow us to do this using the Cache API, which gives us cache objects (a service worker may have any number of caches).

Compared with AppCache, you are swapping the AppCache manifest file for JavaScript. The code is relatively simple, and you’ll save a great deal of time not having to cope with AppCache’s rather complex and frustrating rules of caching. Service workers allow you to decide when resources should be cached, fetched or refreshed, putting you in full control of the user experience.

Generally you’ll have two ways to cache files: cache.addAll(), which takes a list of asset URLs you want to cache; or cache.put(), which takes a request and response object pair. The response object is obtained by requesting an asset from the network using the new fetch() API, making it a pretty vital API for service workers. Before we look at the Fetch API, we need to look at JavaScript promises first, which play an important role in Fetch and service workers.

PROMISES

Promises are a simple way to work with asynchronous events and have the added benefit that they can be chained together, so you only have to care about success or failure. I’m only going to give you a brief overview of how promises work; Jake Archibald has written up a great blog post on JavaScript promises5 which I strongly urge you to check out — and yes, some of this is shamefully lifted from that post (it’s just too good not to).

A promise has a few specific states: it can be pending or settled. If the promise is pending, it means it hasn’t completed its task; if a promise is settled, then it has completed its intended action and has either been resolved or rejected. Resolved means that the promise completed its work successfully, without errors. If a promise is rejected, then either its task could not be completed or an error was thrown unexpectedly.

How does this help us? Well, whenever we need to handle an asynchronous task, we add two callbacks, then() and catch() which relate to resolved and rejected, which you can see below.

somePromise.then(function(arg) {

// The promise resolved successfully

console.log("Success", arg);

}).catch(function(error) {

// The promise rejected

console.error("Failure", error);

});

The advantage of this is that it simplifies code when you compare it to event callbacks, especially when you chain promises together.

Let’s imagine we want to get some JSON from a server and, once we’ve got it, we want to parse it and make a second request with an ID from the first response; then, finally, from this second response we do something fancy. We can do this by chaining the promises like so:

fetch('something.json').then(function(response) {

// Step 1

return JSON.parse(response);

}).then(function(parsedJson) {

// Step 2

return get('somethingelse.json?id=' + parsedJson.id);

}).then(function(response) {

// Step 3

console.log('Woop Woop. We have our second bit of data', response);

}).catch(function(err) {

console.error('Oops. Something went bad.', err);

});

Essentially, we wait for the first call to get('something.json') to succeed, we return the parsed JSON in the first callback, which passes it into the second step of the chain, and note that the value passed in, parsedJson, is the return of JSON.parse. In the second chain we return get('somethingelse.json?id=' + data.id), which for the sake of this example returns a promise. Because we return a promise in this second step, the chain will wait for the returned promise to settle (reject or resolve) before it calls the next step in the chain (in this case, step 3). This is why we can return a promise from get() and in the following then() callback, the response from get('somethingelse.json?id=' + data.id) is passed in.

This looks pretty complex and takes a little getting used to, but once you start following this pattern and become familiar with the Promise API, it makes handling asynchronous code much easier than using event listeners.

Once again, I really do urge you to read Jake’s article on promises. I’ve only covered enough so that we can jump into Fetch and service workers.

We briefly mentioned that a common way to cache resources is to make a request using the fetch() method and caching the response, so let’s look at a real example.

FETCH

You can think of Fetch as an API which allows you to make network requests similar to XHR. The key differences are that it has an easier API, uses promises, and it allows you to make cross-origin requests regardless of the CORS headers — something XHR is unable to do.

Let’s look at a typical fetch() request.

fetch('api/some.json')

.then(function(response) {

// Read the response as text

return response.text();

})

.then(function(text) {

// Print out the responses text

console.log(text);

})

.catch(function(err) {

console.error('Fetch Error :-S', err);

});

We simply pass in a URL to our fetch() call, which returns a promise, and we handle the resolve and rejection.

If the request is successful, the promise will resolve calling the first step in our chain with a response object. If there is an error, the promise will reject and call the catch() function in our chain.

The response body is a stream. This means that the response may still be in progress while we decide how to consume it. When we call response.text(), a promise is returned to take care of the asynchronous retrieval of the stream and, as we learned before, only after this promise is complete will the next step in the promise chain be called, where we log the response text to the console.

If we wanted to make a request for an asset that doesn’t support CORS headers on a different origin, we can do that by defining the mode of the fetch() request.

fetch(url, {'mode': 'no-cors'}).then(function(response) {

if (response.type === 'opaque') {

console.log(“The Response is opaque so we can't examine it”);

// Do something with the response (i.e. cache it for offline support)

return;

}

if (response.status !== 200) {

console.log('Looks like there was a problem. Status Code: ' + response.status);

throw new Error(‘Bad status code’);

}

// Examine the text in the response

return response.text()

.then(function(responseText) {

console.log(responseText);

});

}).catch(function(err) {

console.error('Fetch Error :-S', err);

});

With this call, we’ve passed in a URL and an object with a mode parameter of no-cors. Without the no-cors mode parameter, fetch() will fail when you try to get a resource on a different origin without the CORS headers.

This object can contain other options to alter the kind of request which is made, like making a POST request instead of GET.

There is one caveat with no-cors requests. If you make a request to another origin and it doesn’t have CORS, you’ll get a response, but you won’t be able to examine the contents of the response or see what the status code is. You can still cache and use the response, but there is no certainty that the request was successful. These restrictions are in place for security reasons while allowing you to cache these resources and serve them up from a service worker.

In our example above, we handle no-cors responses differently by checking the response type to see if it’s opaque. We know we can’t read the response’s status or data if the type is opaque. If it’s not opaque, we know the request type is either cors or basic and we can examine the status and response.

The main difference between a basic and cors response is that a CORS request restricts which headers you can read.

I’ve written an article on HTML5Rocks6, which covers a few more examples of the Fetch API including how to send credentials and make POST requests.

ADDING A SERVICE WORKER TO YOUR SITE

We’ve looked at promises and fetch(), which are building blocks we’ll use in our service worker, but before we jump into the code, let’s quickly go over the life cycle of a service worker so that as we introduce each bit of code you’ll know how it fits into the bigger picture of a service worker.

LIFE CYCLE OF A SERVICE WORKER

A simplified view of the service worker life cycle has three main states.

1. Installing

2. Activating

3. Idle or Terminated

A service worker install event is dispatched when a new service worker is registered for a page, before it controls any pages.

The activate event is dispatched when a new service worker takes control of an origin (you can think of an origin as a domain with protocol and port number). This is a good time to clean up your cache or anything else if needed.

Between these and other events, the service worker can be idle, in which case the browser is keeping your service worker alive in the background in case it’s needed; otherwise the browser can terminate your service worker to save resources.

Let’s take a look at adding a service worker to a site and how we use these life cycle events.

Register Your Service Worker

The first step to adding a service worker to your app is to tell the browser about your service worker script, which you do by registering it inside your web app.

if ('serviceWorker' in navigator) {

navigator.serviceWorker.register('/serviceworker.js')

.then(function(registration) {

// Registration was successful :)

console.log('ServiceWorker registration successful');

})

.catch(function(err) {

// Registration failed :(

console.log('ServiceWorker registration failed: ', err);

});

}

In our web application, a user will visit our webpage and this bit of code checks if the Service Worker API exists. If it does, we register our service worker file, in this case called serviceworker.js. This is the clear-cut progressive enhancement step where browsers without service worker support will skip over everything and carry on as normal.

You can call register() as many times as you want and the browser will figure out that it’s the same service worker.

There is one subtlety to the register() method and that comes down to the location of the service worker file on your server. In the example above, the serviceworker.js file is at the root of the domain, which means the service worker can intercept requests for the entire origin. However, if we placed the service worker file under /blog/serviceworker.js, then the service worker would only be able to control pages starting with /blog (e.g. /blog/index.html, /blog/foo/bar/index.html etc.). This is referred to as the scope of the service worker.

The original code example could be written to specify the scope:

if ('serviceWorker' in navigator) {

navigator.serviceWorker.register('/serviceworker.js', {scope: './'})

.then(function(registration) {

// Registration was successful :)

console.log('ServiceWorker registration successful');

})

.catch(function(err) {

// Registration failed :(

console.log('ServiceWorker registration failed: ', err);

});

}

Here, the {scope: './'} is relative to the location of the current page. Using the scope parameter, you could reduce the scope while keeping the service worker at the root of your domain. For the blog example, we can reduce the scope to just /blog* URLs by doing either of the following: register('/serviceworker.js', {scope: './blog/'}); or moving the position of the file to register('/blog/serviceworker.js').

Install Step

After you’ve registered a service worker from your webpage, the browser will download your service worker file in the background before dispatching an install event.

The install event is the perfect time to cache any files that the majority of your users will need or are vital for your site to work offline.

Generally, in the install event you’ll do the following:

1. Open a cache

2. Cache a set of files you know you’ll need

Which you do like so:

var CACHE_NAME = 'my-site-cache-v1';

var urlsToCache = [

'/index.html',

'/styles/main.css',

'/script/main.js'

];

self.addEventListener('install', function(event) {

event.waitUntil(

caches.open(CACHE_NAME)

.then(function(cache) {

console.log('Opened cache');

return cache.addAll(urlsToCache);

})

);

});

What we are doing is adding an event listener for the install event, then when it’s called we open a cache, giving it a name (in this case my-site-cache-v1 through CACHE_NAME), and finally call cache.addAll(urlsToCache), which requests all of the URLs in the urlsToCache array and stores them.

cache.addAll() is in the specification but is not currently implemented in Chrome at the time of writing, so to make it work you can grab the Cache polyfill7 from GitHub.

This was my first time playing around with promises, and if you are the same you might be curious about the event.waitUntil() method. event.waitUntil takes a promise and ensures the service worker waits for the event to settle, meaning the service worker stays alive. Once the install event is complete, the service worker will start to control any pages with this origin when the user next returns to your page (either navigating to a new page or refreshing the page).

The important thing with the install event is that if the promise you return to event.waitUntil() rejects, the entire installation of the service worker will fail and it won’t control your pages. Why? Well, if the caching failed for some reason (e.g. network failure for one of the assets) it could leave your pages without a file they absolutely need. But since the cache.addAll() promise rejects on a failure, it results in the install step failing and prevents the service worker from controlling the page. The flip side of this is that you should only cache what you really need in the install step. More files increase the risk that one might fail and prevent your service worker from installing.

If you wanted to simply try to cache some assets, but still install regardless of whether the caching is successful or not, then you can catch the rejection, preventing event.waitUntil() from catching it.

self.addEventListener('install', function(event) {

// Perform install steps

event.waitUntil(

caches.open(CACHE_NAME)

.then(function(cache) {

console.log('Opened cache');

return cache.addAll(urlsToCache);

})

.catch(function(err) {

// Catch any errors so our SW will still install

console.log('Error occurred while caching install assets');

});

);

});

It’s worth pointing out that the Cache API is completely separate from the HTTP cache. The Cache API is used to programmatically store request/response pairs which you manage. The HTTP cache will only be used when you make a fetch() request, which we’ll look at in the next section.

The self variable is similar to window in pages. It can be used to reference the global scope of a JavaScript worker.

Activate Step

After the service worker has successfully installed, it will dispatch an activate event. This event has little use the first time it’s run after a service worker is installed, but whenever you update your service worker, it’s the perfect point to clean up any cached assets you no longer need.

Sooner or later you’ll need to update your service worker, and when you do, these are the steps that’ll occur behind the scenes:

1. You publish a new version of your service worker script.

2. A user will visit your page.

3. The browser downloads your new service worker file in the background and determines it’s different from the previous one.

4. Once downloaded, the install event will be dispatched in your new service worker. Meanwhile, your old service worker will continue controlling your pages.

5. After the install event is complete, your new service worker will enter a waiting state.

6. When the currently open pages of your site are closed, the old service worker will be killed, and the new service worker will take control of pages opened in future.

7. The new service worker will dispatch an activate event.

The activate event is the perfect point to manage your cache, because if you were to delete any existing caches in the install step, the previous service worker (which will still be controlling any open pages) will no longer be able to make use of that cache.

Imagine we started off with one cache, my-site-cache-v1, to store all of our responses, but later we decide that splitting the cached responses into pages-cache-v1 and blog-posts-cache-v1 to separate static pages and blog posts was a good idea. This would be the ideal scenario to clean up the old cache in the activate step.

self.addEventListener('activate', function(event) {

var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1'];

event.waitUntil(

caches.keys().then(function(cacheNames) {

return Promise.all(

cacheNames.map(function(cacheName) {

if (cacheWhitelist.indexOf(cacheName) === -1) {

return caches.delete(cacheName);

}

})

);

})

);

});

The above code gets all the cache names (or keys) and iterates over them, deleting any cache that isn’t in the cacheWhitelist array.

Now that we’ve covered the life cycle events and installed a service worker, it’s time to make use of our well-managed caches.

FETCH EVENTS

The fetch event is where everything starts to come together. The fetch event is dispatched whenever a request is made from a page controlled by your service worker.

The most basic fetch event logic you could use is to check if the requested resource is in the cache, and if so return the cached response, or otherwise fetch it from the network.

self.addEventListener('fetch', function(event) {

event.respondWith(

caches.match(event.request)

.then(function(response) {

// Cache hit - return response

if (response) {

return response;

}

return fetch(event.request);

}

);

);

});

In this very simple example, we add a fetch event listener and when it gets called we check if we have a cached response by calling caches.match(), passing in the request object. When that resolves we check to see if we have a cached response. If we do, we return this cached response to the browser; otherwise we return the promise from a fetch request which will attempt to get the resource from the network.

Once again, promises are used to determine when the event has been handled. In this case, we pass a promise into event.respondWith() which will wait for the promise to resolve before returning a response to the browser.

This is a basic use case of the fetch event. It assumes we’ve cached some assets during the install event and either used the fetch event to return these cached assets or got the resource from the network.

One subtlety that isn’t clear from the above code is that both the event.request and response objects are streams; once you’ve read part of a stream, you can’t read that part a second time. A common pitfall, as you’ll see in the next example, is that when returning the request and response objects from a method, you can’t easily tell when or if they’ll be consumed. You can easily create a clone of a stream and pass the two objects as necessary to avoid the scenario of a single stream being read more than once.

To show you a slightly more complex example and illustrate this stream issue, let’s cache pages as our users visit them.

self.addEventListener('fetch', function(event) {

event.respondWith(

caches.match(event.request).then(function(response) {

// Cache hit - return response.

if (response) {

return response;

};

return fetch(event.request).then(

function(response) {

// Check if we received a valid response.

if (response.type !== 'basic' || response.status !== 200) {

return response;

}

// IMPORTANT: Clone the response. A response is a stream

// and because we want the browser to consume the response

// as well as the cache, we need

// to clone it so we have two streams.

var responseToCache = response.clone();

caches.open(CACHE_NAME).then(function(cache) {

cache.put(event.request, responseToCache);

});

return response;

}

);

})

);

});

Let’s break up each part of this.

First, we check if the resource is already cached and if it is we return it, which we’ve seen before.

caches.match(event.request)

.then(function(response) {

// Cache hit - return response

if (response) {

return response;

}

If we don’t have the request cached, we call fetch() to get it from the network.

return fetch(event.request).then(

function(response) {

// Check if we received a valid response.

if(response.type === 'opaque' || response.status !== 200) {

return response;

}

// IMPORTANT: Clone the response. A response is a stream

// and because we want the browser to consume the response

// as well as the cache consuming the response, we need

// to clone it so we have two streams.

var responseToCache = response.clone();

caches.open(CACHE_NAME)

.then(function(cache) {

cache.put(event.request, responseToCache);

});

return response;

}

);

When a response is returned by fetch(), the first thing we do is check the type and status. We check for an opaque type or a non-200 status and treat these as assets we don’t want to cache and return them to the browser.

If we want to cache the response, we clone it. This is because we’re going to pass one response stream to the cache, and we return the original response at the end of the function. This results in event.respondWith() passing it to the browser to consume the stream.

Before returning the response from fetch(), we open our cache and put our request and response into the cache.

This is a common pattern to get you to think about the best way to implement caching for your site or web app. The world is your oyster with the fetch event. You might decide to only use the cache as a last resort when the network fails, or perhaps you want to cache specific pages you think your users are going to visit ahead of time.

Key things you need to be mindful of are how to handle slow internet connections and how to handle no internet while the device thinks it’s connected. A service worker does not take into account the state of the internet connection, so you decide how to use the cache and network. Using the cache for every request might result in the user receiving stale information, but getting it very quickly. Serving from the network may lead to really slow responses that fail and result in the use of the cache anyway.

If you aren’t sure of the best use of the network and cache for your use case and want some ideas or code examples, Jake Archibald has a fantastic blog post called “The offline cookbook8” which covers a range of options and is definitely worth a read if you need inspiration.

This is pretty much everything I needed to know to get going with service workers. I’ve been using service workers for a while now, and this feels like a good spot to give you some tips and tricks I’ve learned along the way.

TIPS AND TRICKS

These are some tips I wish I’d known when starting to develop with service workers.

CACHE CAREFULLY

Given the choice of caching everything or just specific things, always go for the specific things. It may seem like a great idea to cache anything and everything you can, but there are scenarios where this is inappropriate. Think of pages requiring sign in, or calls to RESTful APIs: do you really want to serve up cached versions? There are plenty of cases where caching is useful for these scenarios, but make sure you test and handle any specific needs.

It’s easier to cache a few specific assets up-front and add extra non-essential things later.

CTRL + SHIFT + R

When you’re developing in Chrome (and hopefully other browsers), you can force a hard refresh (Ctrl + Shift + R on Windows and Linux; Cmd + Shift + R on Mac OS X) to prevent the browser from using the HTTP cache or service worker. This allows you to test the registration of your service worker, allow a new service worker to take control of a page, or force new HTML content and assets to be pulled from the network to see if the cached version is working or not.

IMPORTSCRIPTS()

If you need to pull any JavaScript files into your service worker you can do so with importScripts('./js/script-to-import.js'). This works in a similar way to <script> elements in HTML: the included JavaScript becomes available in the global scope. Browsers will automatically cache these files for you and make them available in your service worker.

This can be a great way to split up your code into logical JavaScript files, helping to organize your code.

CONSTRAINTS AND RESTRICTIONS OF SERVICE WORKERS

One requirement of service workers is HTTPS, and there is one clear reason for this.

You’ve probably noticed that any time you use almost any major web application (mail, social media and so on) HTTPS is used. This is because with simple HTTP it’s trivially easy to intercept traffic, and inspect and even change the communication between the browser and server. Since service workers persist on a device once they’ve been installed, they are particularly vulnerable, even after you’ve left a Wi-Fi network which has been intercepting your traffic. A service worker installed or altered while using a compromised network will continue to persist on the user’s device.

To use service workers, you’ll need to serve content over HTTPS. Fortunately, this is becoming fairly straightforward. It’s increasingly clear that Google is encouraging the use of HTTPS (calling for “HTTPS Everywhere9” at Google I/O 2014, and announcing that HTTPS will be regarded as a signal of quality in search results10).

The major obstacle to HTTPS for many sites is that third parties use HTTP and including these resources in a secure page will result in a mixed content warning, with browsers likely to block that content. In many cases the third party service will offer an HTTPS version; if they don’t, however, you’ll have to consider your strategy for when and how to move to HTTPS before implementing service workers.

CURRENT SUPPORT

Service workers are still in an early stage of development, but Chrome has support for everything discussed in this chapter. Nightly builds of Firefox have implemented aspects of the service worker specification, and other browsers will hopefully start working on it soon.

Owing to the way service workers operate outside the page, you can treat them as a progressive enhancement and start using them in your web apps today.

FUTURE OF SERVICE WORKERS

While the service worker specification is still being developed, part of its purpose is to cater for new use cases and answer developer feedback. The core of the API which we’ve looked at in this chapter is unlikely to change. Instead, new methods are being added to provide extra functionality and behaviors to service workers, which I’m sure we’ll all learn more about as they become available.

While we’ve focused on offline support, it’s worth highlighting that service workers are required for a range of new features that will be coming to the web soon, including push notifications11, background sync, and geofencing.