Service Worker Development Cookbook
上QQ阅读APP看书,第一时间看更新

Displaying a custom offline page

Let's revisit the scenario from the first chapter where you are on a train, traveling home from work, and you are reading an important news article on the web using your mobile device. At the same moment that you click on a link to view more details, the train suddenly disappears into a tunnel. You've just lost connectivity, and are presented with the Unable to connect to the Internet message. Well, you will not doubt be less annoyed if you can still play the dinosaur game by hitting the spacebar on your desktop/laptop, or by tapping on your phone, but this can be an area where you can significantly enhance a client's user experience by using a service worker. One of the great features of service workers is that they allow you to intercept network requests and decide how you want to respond:

In this recipe, we are going to use a service worker to check whether a user has connectivity, and respond with a really simple offline page if they aren't connected.

Getting ready

To get started with service workers, you will need to have the service worker experiment feature turned on in your browser settings. If you have not done this yet, refer to the Setting up service workers recipe of Chapter 1, Learning Service Worker Basics. Service workers only run across HTTPS. To find out how to set up a development environment to support this feature, refer to the following recipes of Chapter 1, Learning Service Worker Basics: Setting up GitHub pages for SSL, Setting up SSL for Windows, and Setting up SSL for Mac.

How to do it...

Follow these instructions to set up your file structure:

  1. First, we must create an index.html file as follows:
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Custom Offline Page</title>
    </head>
    <body>
      <p>Registration status: <strong id="status"></strong></p>
    
      <script>
        var scope = {
          scope: './'
        };
    
        if ('serviceWorker' in navigator) {
          navigator.serviceWorker.register('service-worker.js', scope)
          .then(
            function(serviceWorker) {
            document.getElementById('status').innerHTML = 'successful';
          }).catch(function(error) {
            document.getElementById('status').innerHTML = error;
          });
        } else {
            document.getElementById('status').innerHTML = 'unavailable';
          }
      </script>
    </body>
    </html>
  2. Create a JavaScript file called service-worker.js in the same folder as the index.html file, with the following code:
    'use strict';
    
    var version = 1;
    var currentCache = {
      offline: 'offline-cache' + version
    };
    
    var offlineUrl = 'offline.html';
    
    self.addEventListener('install', function(event) {
      event.waitUntil(
        caches.open(currentCache.offline).then(function(cache) {
          return cache.addAll([
            offlineUrl
          ]);
        })
      );
    });
    
    self.addEventListener('fetch', function(event) {
      var request = event.request,
        isRequestMethodGET = request.method === 'GET';
    
      if (request.mode === 'navigate' || isRequestMethodGET) {
        event.respondWith(
          fetch(createRequestWithCacheBusting(request.url)).catch(function(error) {
            console.log('OFFLINE: Returning offline page.', error);
            return caches.match(offlineUrl);
          })
        );
      } else {
        event.respondWith(caches.match(request)
            .then(function (response) {
            return response || fetch(request);
          })
        );
      }
    });
    function createRequestWithCacheBusting(url) {
      var request,
        cacheBustingUrl;
    
      request = new Request(url,
        {cache: 'reload'}
      );
    
      if ('cache' in request) {
        return request;
      }
    
      cacheBustingUrl = new URL(url, self.location.href);
      cacheBustingUrl.search += (cacheBustingUrl.search ? '&' : '') + 'cachebust=' + Date.now();
    
      return new Request(cacheBustingUrl);
    }
  3. Create a second HTML file called offline.html file as follows:
    <!DOCTYPE html>
    <html>
     <head>
      <meta charset="UTF-8">
      <title>Offline</title>
      <style>
        #container {
          text-align: center;
          margin-top: 40px;
        }
        #container img {
          width: 80px;
          height: 80px;
        }
      </style>
     </head>
     <body>
       <div id="container">
         <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25">
           <path d="M16 0l-3 9h9l-1.866 2h-14.4L16 0zm2.267 13h-14.4L2 15h9l-3 9 10.267-11z" fill="#04b8b8"/>
         </svg>
         <p>Whoops, something went wrong...!</p>
         <p>Your internet connection is not working.</p>
         <p>Please check your internet connection and try again.</p>
       <div>
      </body>
    </html>
  4. Open up a browser and go to index.html. You will see the Registration status: successful message:
  5. Now open up DevTools (Cmd + Alt + I or F12), go to the Network tab, click on the dropdown displaying No throttling, and select Offline:
  6. Now refresh your browser, and you will see the offline message and the following image:

How it works...

When the registration is successful, we are instructing the service worker to intercept a request and provide resources from the cached content using the fetch event, as illustrated in the following diagram:

Inside the index.html file, when the registration is successful, we inspect the state of the registration and print it to the browser. Otherwise, we are printing the error message returned by the service worker:

navigator.serviceWorker.register(
      'service-worker.js',
      { scope: './' }
   ).then(function(serviceWorker) {
      document.getElementById('status').innerHTML = 
          'successful';
   }).catch(function(error) {
      document.getElementById('status').innerHTML = error;
   });

The service worker script file will intercept network requests, check for connectivity, and provide the content to the user.

We start off by adding our offline page to the cache when we install the service worker. In the first few lines, we are specifying the cache version and the URL for the offline page. If we had different versions of our cache, you would simply update this version number, so a new version of the file will take effect. We call this cache busting:

var version = 1;
var currentCache = {
  offline: 'offline-cache' + version
};

We add an event listener to the install event and inside the callback, we make a request for this offline page and its resources; when we have a successful response, it gets added to the cache:

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(currentCache.offline)
    .then(function(cache) {
         return cache.addAll([
            offlineUrl
         ]);
    })
  );
});

Now that the offline page is stored in the cache, we can retrieve it whenever we need to. In the same service worker, we need to add the logic to return the offline page if we have no connectivity:

self.addEventListener('fetch', function(event) {
  var request = event.request,
    isRequestMethodGET = request.method === 'GET';

  if (request.mode === 'navigate' || isRequestMethodGET) {
    event.respondWith(
      fetch(createRequestWithCacheBusting(request.url)).catch(function(error) {
        console.log('OFFLINE: Returning offline page.', error);
        return caches.match(offlineUrl);
      })
    );
  } else {
    event.respondWith(caches.match(request)
        .then(function (response) {
        return response || fetch(request);
      })
    );
  }
}); 

In the preceding listing, we are listening out for the fetch event, and if we detect that the user is trying to navigate to another page, and there is an error while doing so, we simply return the offline page from the cache. And there you go, we have our offline page working.

There's more...

The waitUntil event extends the lifetime of the install event, until all the caches are populated. In other words, it delays treating the installing worker as installed, until all the resources we specify are cached and the passed promise resolves successfully.

We saw an HTML and an image file get cached, and then being retrieved when our website is offline. We can cache other resources as well, including CSS and JavaScript files:

caches.open(currentCache.offline)
.then(function(cache) {
    return cache.addAll([
        'offline.html',
        '/assets/css/style.css',
        '/assets/js/index.js'
    ]);
  })
);

See also

  • The Registering a service worker in detail recipe of Chapter 1, Learning Service Worker Basics
  • The Creating mock responses recipe of Chapter 1, Learning Service Worker Basics