|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- 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">
- var CACHE_NAME = 'dependencies-cache';
-
- // Files required to make this app work offline
- var REQUIRED_FILES = [
- 'random-1.png',
- 'random-2.png',
- 'random-3.png',
- 'random-4.png',
- 'random-5.png',
- 'random-6.png',
- 'style.css',
- 'index.html',
- '/', // Separate URL than index.html!
- 'index.js',
- 'app.js'
- ];
- </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">
- self.addEventListener('install', function(event) {
- // Perform install step: loading each required file into cache
- event.waitUntil(
- caches.open(CACHE_NAME)
- .then(function(cache) {
- // Add all offline dependencies to the cache
- return cache.addAll(REQUIRED_FILES);
- })
- .then(function() {
- // At this point everything has been cached
- return self.skipWaiting();
- })
- );
- });
- </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">
- self.addEventListener('fetch', function(event) {
- event.respondWith(
- caches.match(event.request)
- .then(function(response) {
- // Cache hit - return the response from the cached version
- if (response) {
- return response;
- }
-
- // Not in cache - return the result from the live server
- // `fetch` is essentially a "fallback"
- return fetch(event.request);
- }
- )
- );
- });
- </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">
- self.addEventListener('activate', function(event) {
- // Calling claim() to force a "controllerchange" event on navigator.serviceWorker
- event.waitUntil(self.clients.claim());
- });
- </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">
- // Register the ServiceWorker
- navigator.serviceWorker.register('service-worker.js', {
- scope: '.'
- }).then(function(registration) {
- // The service worker has been registered!
- });
- </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">
- // Listen for claiming of our ServiceWorker
- navigator.serviceWorker.addEventListener('controllerchange', function(event) {
- // Listen for changes in the state of our ServiceWorker
- navigator.serviceWorker.controller.addEventListener('statechange', function() {
- // If the ServiceWorker becomes "activated", let the user know they can go offline!
- if (this.state === 'activated') {
- // Show the "You may now use offline" notification
- document.getElementById('offlineNotification').classList.remove('hidden');
- }
- });
- });
- </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">
- // This file is required to make the "app" work offline
- document.querySelector('#randomButton').addEventListener('click', function() {
- var image = document.querySelector('#logoImage');
- var currentIndex = Number(image.src.match('random-([0-9])')[1]);
- var newIndex = getRandomNumber();
-
- // Ensure that we receive a different image than the current
- while (newIndex === currentIndex) {
- newIndex = getRandomNumber();
- }
-
- image.src = 'random-' + newIndex + '.png';
-
- function getRandomNumber() {
- return Math.floor(Math.random() * 6) + 1;
- }
- });
- </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">
- self.addEventListener('install', function(event) {
- // Put `offline.html` page into cache
- var offlineRequest = new Request('offline.html');
- event.waitUntil(
- fetch(offlineRequest).then(function(response) {
- return caches.open('offline').then(function(cache) {
- return cache.put(offlineRequest, response);
- });
- })
- );
- });
- </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">
- self.addEventListener('fetch', function(event) {
- // Only fall back for HTML documents.
- var request = event.request;
- // && request.headers.get('accept').includes('text/html')
- if (request.method === 'GET') {
- // `fetch()` will use the cache when possible, to this examples
- // depends on cache-busting URL parameter to avoid the cache.
- event.respondWith(
- fetch(request).catch(function(error) {
- // `fetch()` throws an exception when the server is unreachable but not
- // for valid HTTP responses, even `4xx` or `5xx` range.
- return caches.open('offline').then(function(cache) {
- return cache.match('offline.html');
- });
- })
- );
- }
- // Any other handlers come here. Without calls to `event.respondWith()` the
- // request will be handled without the ServiceWorker.
- });
- </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">
- if (navigator.serviceWorker.controller) {
- // A ServiceWorker controls the site on load and therefor can handle offline
- // fallbacks.
- console.log('DEBUG: serviceWorker.controller is truthy');
- debug(navigator.serviceWorker.controller.scriptURL + ' (onload)', 'controller');
- }
-
- else {
- // Register the ServiceWorker
- console.log('DEBUG: serviceWorker.controller is falsy');
- navigator.serviceWorker.register('service-worker.js', {
- scope: './'
- }).then(function(reg) {
- debug(reg.scope, 'register');
- });
- }
- </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">
- // The refresh link needs a cache-busting URL parameter
- document.querySelector('#refresh').search = Date.now();
- </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>
|