title: Offline Recipes for Service Workers
url: https://hacks.mozilla.org/2015/11/offline-service-workers/
hash_url: 855debdfdc494d53ccf8e1939ea85362

<p>“Offline” is a big topic these days, especially as many web apps look to also function as mobile apps.  The original offline helper API, the Application Cache API (also known as “appcache”), has a host of problems, many of which can be found in Jake Archibald’s <a href="http://alistapart.com/article/application-cache-is-a-douchebag" target="_blank">Application Cache is a Douchebag</a> post.  Problems with appcache include:</p>
<ul>
<li>Files are served from cache even when the user is online.</li>
<li>There’s no dynamism: the appcache file is simply a list of files to cache.</li>
<li>One is able to <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache#Gotchas" target="_blank">cache the .appcache file itself</a> and that leads to update problems.</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache#Gotchas" target="_blank">Other gotchas</a>.</li>
</ul>
<p>Today there’s a new API available to developers to ensure their web apps work properly:  <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API" target="_blank">the Service Worker API</a>.  The Service Worker API allows developers to manage what does and doesn’t go into cache for offline use with JavaScript.</p>
<h2>Introducing the Service Worker Cookbook</h2>
<p>To introduce you to the Service Worker API we’ll be using examples from Mozilla’s new  <a href="https://serviceworke.rs" target="_blank">Service Worker Cookbook</a>!  The Cookbook is a collection of working, practical examples of service workers in use in modern web apps.  We’ll be introducing service workers within this three-part series:</p>
<ul>
<li>Offline Recipes for Service Workers (today’s post)</li>
<li>At Your Service for More Than Just appcache</li>
<li>Web Push Updates to the Masses</li>
</ul>
<p>Of course this API has advantages other than enabling offline capabilities, such as performance for one, but I’d like to start by introducing basic service worker strategies for offline.</p>
<h2>What do we mean by <em>offline</em>?</h2>
<p>Offline doesn’t just mean the user doesn’t have an internet connection — it can also mean that the user is on a flaky network connection.  Essentially “offline” means that the user doesn’t have a reliable connection, and we’ve all been there before!</p>

<p>The Offline Status recipe illustrates how to use a service worker to cache a known asset list and then notify the user that they may now go offline and use the app.  The app itself is quite simple:  show a random image when a button is clicked.  Let’s have a look at the components involved in making this happen.</p>
<h3>The Service Worker</h3>
<p>We’ll start by looking at the <code>service-worker.js</code> file to see what we’re caching.  We’ll be caching the random images to display, as well as the display page and critical JavaScript resources, in a cache named <code>dependencies-cache</code>:</p>
<pre><code class="language-javascript">&#13;
var CACHE_NAME = 'dependencies-cache';&#13;
&#13;
// Files required to make this app work offline&#13;
var REQUIRED_FILES = [&#13;
  'random-1.png',&#13;
  'random-2.png',&#13;
  'random-3.png',&#13;
  'random-4.png',&#13;
  'random-5.png',&#13;
  'random-6.png',&#13;
  'style.css',&#13;
  'index.html',&#13;
  '/', // Separate URL than index.html!&#13;
  'index.js',&#13;
  'app.js'&#13;
];&#13;
</code></pre>
<p>The service worker’s <code>install</code> event will open the cache and use <code>addAll</code> to direct the service worker to cache our specified files:</p>
<pre><code class="language-javascript">&#13;
self.addEventListener('install', function(event) {&#13;
  // Perform install step:  loading each required file into cache&#13;
  event.waitUntil(&#13;
    caches.open(CACHE_NAME)&#13;
      .then(function(cache) {&#13;
        // Add all offline dependencies to the cache&#13;
        return cache.addAll(REQUIRED_FILES);&#13;
      })&#13;
      .then(function() {&#13;
      	// At this point everything has been cached&#13;
        return self.skipWaiting();&#13;
      })&#13;
  );&#13;
});&#13;
</code></pre>
<p>The <code>fetch</code> event of a service worker is fired for every single request the page makes.  The <code>fetch</code> event also allows you to serve alternate content than was actually requested.  For the purposes of offline content, however, our <code>fetch</code> listener will be very simple:  if the file is cached, return it from cache; if not, retrieve the file from server:</p>
<pre><code class="language-javascript">&#13;
self.addEventListener('fetch', function(event) {&#13;
  event.respondWith(&#13;
    caches.match(event.request)&#13;
      .then(function(response) {&#13;
        // Cache hit - return the response from the cached version&#13;
        if (response) {&#13;
          return response;&#13;
        }&#13;
&#13;
        // Not in cache - return the result from the live server&#13;
        // `fetch` is essentially a "fallback"&#13;
        return fetch(event.request);&#13;
      }&#13;
    )&#13;
  );&#13;
});&#13;
</code></pre>
<p>The last part of this <code>service-worker.js</code> file is the <code>activate</code> event listener where we immediately claim the service worker so that the user doesn’t need to refresh the page to activate the service worker.  The <code>activate event</code> fires when a previous version of a service worker (if any) has been replaced and the updated service worker takes control of the scope.</p>
<pre><code class="language-javascript">&#13;
self.addEventListener('activate', function(event) {&#13;
  // Calling claim() to force a "controllerchange" event on navigator.serviceWorker&#13;
  event.waitUntil(self.clients.claim());&#13;
});&#13;
</code></pre>
<p>Essentially we don’t want to require the user to refresh the page for the service worker to begin — we want the service worker to activate upon initial page load.</p>
<h3>Service worker registration</h3>
<p>With the simple service worker created, it’s time to register the service worker:</p>
<pre><code class="language-javascript">&#13;
// Register the ServiceWorker&#13;
navigator.serviceWorker.register('service-worker.js', {&#13;
  scope: '.'&#13;
}).then(function(registration) {&#13;
  // The service worker has been registered!&#13;
});&#13;
</code></pre>
<p>Remember that the goal of the recipe is to notify the user when required files have been cached.  To do that we’ll need to listen to the service worker’s <code>state</code>. When the <code>state</code> has become <code>activated</code>, we know that essential files have been cached, our app is ready to go offline, and we can notify our user:</p>
<pre><code class="language-javascript">&#13;
// Listen for claiming of our ServiceWorker&#13;
navigator.serviceWorker.addEventListener('controllerchange', function(event) {&#13;
  // Listen for changes in the state of our ServiceWorker&#13;
  navigator.serviceWorker.controller.addEventListener('statechange', function() {&#13;
    // If the ServiceWorker becomes "activated", let the user know they can go offline!&#13;
    if (this.state === 'activated') {&#13;
      // Show the "You may now use offline" notification&#13;
      document.getElementById('offlineNotification').classList.remove('hidden');&#13;
    }&#13;
  });&#13;
});&#13;
</code></pre>
<p>Testing the registration and verifying that the app works offline simply requires using the recipe!  This recipe provides a button to load a random image by changing the image’s <code>src</code> attribute:</p>
<pre><code class="language-javascript">&#13;
// This file is required to make the "app" work offline&#13;
document.querySelector('#randomButton').addEventListener('click', function() {&#13;
  var image = document.querySelector('#logoImage');&#13;
  var currentIndex = Number(image.src.match('random-([0-9])')[1]);&#13;
  var newIndex = getRandomNumber();&#13;
&#13;
  // Ensure that we receive a different image than the current&#13;
  while (newIndex === currentIndex) {&#13;
    newIndex = getRandomNumber();&#13;
  }&#13;
&#13;
  image.src = 'random-' + newIndex + '.png';&#13;
&#13;
  function getRandomNumber() {&#13;
    return Math.floor(Math.random() * 6) + 1;&#13;
  }&#13;
});&#13;
</code></pre>
<p>Changing the image’s <code>src</code> would trigger a network request for that image, but since we have the image cached by the service worker, there’s no need to make the network request.</p>
<p>This recipe covers probably the most simple of offline cases: caching required static files for offline use.</p>

<p>This recipe follows another simple use case: fetch a page via AJAX but respond with another cached HTML resource (<code>offline.html</code>) if the request fails.</p>
<h3>The service worker</h3>
<p>The <code>install</code> step of the service worker fetches the <code>offline.html</code> file and places it into a cache called <code>offline</code>:</p>
<pre><code class="language-javascript">&#13;
self.addEventListener('install', function(event) {&#13;
  // Put `offline.html` page into cache&#13;
  var offlineRequest = new Request('offline.html');&#13;
  event.waitUntil(&#13;
    fetch(offlineRequest).then(function(response) {&#13;
      return caches.open('offline').then(function(cache) {&#13;
        return cache.put(offlineRequest, response);&#13;
      });&#13;
    })&#13;
  );&#13;
});&#13;
</code></pre>
<p>If that requests fails the service worker won’t register since nothing has been put into cache.</p>
<p>The <code>fetch</code> listener listens for a request for the page and, upon failure, responds with  the <code>offline.html</code> file we cached during the event registration:</p>
<pre><code class="language-javascript">&#13;
self.addEventListener('fetch', function(event) {&#13;
  // Only fall back for HTML documents.&#13;
  var request = event.request;&#13;
  // &amp;&amp; request.headers.get('accept').includes('text/html')&#13;
  if (request.method === 'GET') {&#13;
    // `fetch()` will use the cache when possible, to this examples&#13;
    // depends on cache-busting URL parameter to avoid the cache.&#13;
    event.respondWith(&#13;
      fetch(request).catch(function(error) {&#13;
        // `fetch()` throws an exception when the server is unreachable but not&#13;
        // for valid HTTP responses, even `4xx` or `5xx` range.&#13;
        return caches.open('offline').then(function(cache) {&#13;
          return cache.match('offline.html');&#13;
        });&#13;
      })&#13;
    );&#13;
  }&#13;
  // Any other handlers come here. Without calls to `event.respondWith()` the&#13;
  // request will be handled without the ServiceWorker.&#13;
});&#13;
</code></pre>
<p>Notice we use <code>catch</code> to detect if the request has failed and that therefore we should respond with <code>offline.html</code> content.</p>
<h3>Service Worker Registration</h3>
<p>A service worker needs to be registered only once. This example shows how to bypass registration if it’s already been done by checking the presence of the <code>navigator.serviceWorker.controller</code> property; if the <code>controller</code> property doesn’t exist, we move on to registering the service worker.</p>
<pre><code class="language-javascript">&#13;
if (navigator.serviceWorker.controller) {&#13;
  // A ServiceWorker controls the site on load and therefor can handle offline&#13;
  // fallbacks.&#13;
  console.log('DEBUG: serviceWorker.controller is truthy');&#13;
  debug(navigator.serviceWorker.controller.scriptURL + ' (onload)', 'controller');&#13;
}&#13;
&#13;
else {&#13;
  // Register the ServiceWorker&#13;
  console.log('DEBUG: serviceWorker.controller is falsy');&#13;
  navigator.serviceWorker.register('service-worker.js', {&#13;
    scope: './'&#13;
  }).then(function(reg) {&#13;
    debug(reg.scope, 'register');&#13;
  });&#13;
}&#13;
</code></pre>
<p>With the service worker confirmed as registered, you can test the recipe (and trigger the new page request) by clicking the “refresh” link: (which then triggers a page refresh with a cache-busting parameter):</p>
<pre><code class="language-javascript">&#13;
// The refresh link needs a cache-busting URL parameter&#13;
document.querySelector('#refresh').search = Date.now();&#13;
</code></pre>
<p>Providing the user an offline message instead of allowing the browser to show its own (sometimes ugly) message is an excellent way of keeping a dialog with the user about why the app isn’t available while they’re offline!</p>
<h2>Go offline!</h2>
<p>Service workers have moved offline experience and control into a powerful new space.  Today you can use the Service Worker API in Chrome and <a href="https://www.mozilla.org/en-US/firefox/developer/" target="_blank">Firefox Developer Edition</a>.  Many websites are using service workers today as you can see for yourself by going to <code>about:serviceworkers</code> in Firefox Developer Edition;  you’ll see a listing of installed service workers from websites you’ve visited!</p>
<p><a href="https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2015/11/AboutServiceWorkers.png"><img src="https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2015/11/AboutServiceWorkers.png" alt="about:serviceworkers"/></a></p>
<p>The <a href="https://serviceworke.rs" target="_blank">Service Worker Cookbook</a> is full of excellent, practical recipes and we continue to add more.  Keep an eye out for the next post in this series, <em>At Your Service for More than Just appcache</em>, where you’ll learn about using the Service Worker API for more than just offline purposes.</p>
