Chapter 2. Your First Service Worker

Setting Up Our Sample Project

This book takes a hands-on approach to learning about progressive web apps.

Starting with this chapter, we will take a simple web app for the fictional Gotham Imperial Hotel and improve on it chapter by chapter. Each chapter builds and improves upon the work done in the previous one, and at the end of each chapter you will have a working web app that is ready to ship.

By the end of the book, you will have taken this simple website and turned it into a fully featured progressive web app.

In order to follow along with the code samples, and play around with them, you can clone the source code of the app onto your local machine. You can find the code in the Gotham Imperial Hotel GitHub repository.

Note that you will need to be able to run Git, Node.js, and NPM on your local machine in order to clone the code and run it locally. If you are unable to run any of these, it should be possible to do everything in the book without them (downloading the source code directly from GitHub, and running it on a remote server), but I do not recommend it.

To get started, open your computer’s command prompt (the console), change to the directory you would like to download the code to, then run the following commands:

git clone -b ch02-start [email protected]:TalAter/gotham_imperial_hotel.git
cd gotham_imperial_hotel
npm install

These commands will clone the source code for the Gotham Imperial Hotel web app, change to the branch named ch02-start, and install the dependencies needed to run it.

Next, you can go ahead and start a local server to serve the site to your browser with the following command:

npm start

If you now open http://localhost:8443/ in your browser, you should see the Gotham Imperial Hotel web app.

Caution

If the web app does not load in your browser, please make sure of the following:

  • You have Git, Node.js, and NPM installed and you are able to use them from the command line (e.g., Terminal or iTerm in macOS; Windows Command Prompt or Cygwin in Windows).

  • You have followed all of the preceding steps.

If you are still having trouble running the app, feel free to ask for help in our issue tracker.

You can now open the project in your favorite IDE or editor and follow along with the book as you transform this site into a progressive web app.

As the code of each chapter builds on changes made in previous chapters, at the beginning of each chapter your code will need to include all of those changes. If you skip over any of the coding exercises in the book, or even whole chapters, you can always bring the code to the state it should be in at the beginning of each chapter by running the following two commands in the command line:

git reset --hard
git checkout ch04-start

These commands will reset all changes done locally, then check out the branch with all the changes done before that chapter. Make sure to change the name of the branch in the second command to the name of the chapter you are currently in. For example, when you are starting Chapter 6, run git checkout ch06-start and this will check out a branch containing all of the changes done in the first five chapters.

Welcome to the Gotham Imperial Hotel

The project that will escort our journey to discover progressive web apps is the website of the fictional Gotham Imperial Hotel.

This simple site contains two pages:

  1. A home page containing information about the hotel, a map, a list of upcoming events, and a form to make new reservations.

  2. A My Account page containing a list of the user’s reservations, upcoming events, and a form to make new reservations.

While simple, these two pages contain most of the elements that make up both content-focused sites, as well as more app-like web applications.

As you read through the book, you will take this simple site and turn it into a full-featured progressive web app.

Different Challenges, Different Approaches

As we explore the different features that make up a progressive web app, we will occasionally take a step back from the Gotham Imperial Hotel app and explore the same ideas in a different context.

While our hotel’s app resembles a more traditional business website, these notes will explore similar challenges from the perspective of an app that more closely resembles a traditional native app. By exploring the differences and similarities in approaches, we can gain a better understanding of how each feature can fit different projects, and how different businesses can benefit from each new feature.

msger, the fictional messaging app we will explore in these notes, allows users to post 140-character messages, and displays a stream of recent user messages. As new messages appear, they are added to the top of the message stream, pushing older messages to the bottom (Figure 2-1).

bpwa 0201
Figure 2-1. Our sample messaging app

Getting to Know the Code

Before we begin, let’s familiarize ourselves with the basic code structure of the app.

Within the main project directory are the two most important directories:

public

Contains all of the client-side code of the site, as well as all the other files needed to run it (e.g., images and stylesheets).

server

Contains the code of the server that serves the site, keeps track of reservations, sends notifications, and more.

All of the coding exercises in the book will only involve the public directory, but you may want to peek into the server directory from time to time—especially in Chapter 10.

First, a Word About the Code

If you look at the starting state of the app’s code, you will see that it has been kept quite simple. It often trades off best practices, and even common sense, for readability and the opportunity to clearly demonstrate the key principles we will learn.

By the time you are finished with this book, you will get a chance to improve much of this code and hopefully learn not only how to build a progressive web app from scratch, but also how an existing project can be improved and turned into a progressive web app.

I have chosen not to use many of the modern ES2015 language constructs in this book so that you, the reader, could focus on the book’s subject matter, not on new syntax that may or may not be familiar to you. To see how the code in this book could benefit from ES2015, see Appendix A.

The Current Offline Experience

Having completed the previous section, you should now have a copy of the Gotham Imperial Hotel web app and a local web server that can run it.

To make sure the code you are working on is at the state it should be in at the beginning of this chapter, run the following in the command line:

git reset --hard
git checkout ch02-start

Next, run the command npm start to start a local web server running the site, and open it in your browser (http://localhost:8443/). You should be able to see the site in all of its glory (Figure 2-2).

bpwa 0202
Figure 2-2. The Gotham Imperial Hotel home page

This is the web as it is today. Rich, beautiful, and useful. Actually, this is the web as it is today for you—the developer. As developers, we usually look at our sites from a relatively modern desktop, laptop, or mobile device. We have a reliable connection either to a local server, or a development server in close proximity to us. But our users might be experiencing our web apps very differently than us. What happens when a user visits our web app when he is offline (Figure 2-3)?

bpwa 0203
Figure 2-3. Our sample web app, as experienced by a user in an elevator

Unfortunately, for many of your users, this is the web as it is today. Service workers finally let us do something about it.

Simulating an Offline State

While working on the sample app throughout this book, you will often need to simulate an offline state. Since an offline state is essentially the user being unable to reach your server, one way to simulate this state is by taking down our development server.

In the command line where your local server is running, press Ctrl+C to terminate the server. Now reload the app in your browser to see what it looks like when the user is offline.

When you are ready to “go online” again, just run npm start again.

This basic method works well for simulating an offline state during development. But once you release your code to production, taking down the production server every time you want to test something is not feasible. Thankfully, most modern browsers include tools for simulating an offline state and even different connection speeds (Figure 2-4). See the section on “Developer Tools” for more details.

bpwa 0204
Figure 2-4. Simulating an offline state in Google Chrome

Creating Your First Service Worker

Let’s take control of our user’s offline experience.

We begin by registering a new service worker for the current page. Open the js/app.js file and add the following code at the top of the file:

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/serviceworker.js")
    .then(function(registration) {
      console.log("Service Worker registered with scope:", registration.scope);
    }).catch(function(err) {
      console.log("Service worker registration failed:", err);
    });
}

The code begins by verifying that the current browser supports service workers. It then registers our service worker by calling navigator.serviceWorker.register, which takes two arguments. The first is the URL of our service worker script. The second is an optional options object (which we have omitted here, but will explore later in this chapter in “Understanding Service Worker Scope”).

By testing for service worker support before using it, we make sure we are not excluding users of older browsers from using our app, while offering an enhanced experience to users of more modern browsers. This practice of progressive enhancement is central to how we will build our app (see “What Is Progressive Enhancement?”).

The register call returns a promise. If the promise is fulfilled, meaning the service worker was registered successfully, the function defined in the then statement is called. If there was any problem, the function defined inside the catch block would be executed.

If you refresh the sample app in your browser, you should see an error message in your browser’s console telling you that “Service worker registration failed.”1

The service worker registration failed and the promise was rejected because we have not created our serviceworker.js file yet.

Create an empty file called serviceworker.js, and place it at the root of your project’s public directory (i.e., public/serviceworker.js). If you refresh your browser, you should receive a message telling you “Service worker registered with scope: http://localhost:8443/”. Even though our service worker is nothing but an empty file, it is still a valid service worker and was registered successfully.

Caution

You may be tempted to move the serviceworker.js file into the js subdirectory of the project. Keep it in the root directory for now. You will learn why this is important in the section “Understanding Service Worker Scope”.

Let’s begin our exploration of what the service worker can do.

Add the following code to serviceworker.js:

self.addEventListener("fetch", function(event) {
  console.log("Fetch request for:", event.request.url);
});

This code adds an event listener to our service worker by calling addEventListener on self (self within a service worker refers to the service worker itself). This listener will listen to all fetch events that pass through the service worker and run the function we define next, passing it an event object as its sole argument. The function we define to handle these events accesses the request object (available as a property of the fetch event) and logs the URL of that request.

Refresh the page, and you should now see that the URL of every request made by the page gets logged to the browser’s console. (If you do not see any URLs in the console, the old empty service worker might still be in control. See “Service Worker Lifecycle” for tips.)

While this may not seem very impressive at first, consider this: every request made by our page (including to third-party servers) now passes through our service worker. All of those requests can now be intercepted, analyzed, and even manipulated.

Let’s look at an example of how powerful this feature is.

Replace the code in serviceworker.js with the following, and refresh your browser:

self.addEventListener("fetch", function(event) {
  if (event.request.url.includes("bootstrap.min.css")) {
    event.respondWith(
      new Response(
        ".hotel-slogan {background: green!important;} nav {display:none}",
        { headers: { "Content-Type": "text/css" }}
      )
    );
  }
});

This code listens for fetch events, and examines each request’s URL to see if it contains the string bootstrap.min.css. If it does, instead of fetching this file from a remote server, the service worker will respond to the request with a new Response it creates on the fly, containing custom CSS (Figure 2-6).

Our sample web app with a modified CSS response
Figure 2-6. Overwriting a request for the CSS and modifying background color

In just a few lines of JavaScript, we were able to create a service worker that intercepted a request to a third-party server, made up a new response out of thin air, and presented it to the browser as if it came from that server. We essentially created a proxy server inside the browser.

Browser Support for Service Workers

While the specification was only published in 2014, browsers have adopted service workers surprisingly fast. By late 2015, Chrome, Opera, Firefox, and Samsung Internet have all added support for service workers.

At the time of this book’s publication, the WebKit team is working on bringing service workers to iPhones and all Safari flavored browsers, as is the Microsoft Edge team.

For an up-to-date status of browser support for service worker, and related technologies, visit Jake Archibald’s web page “Is ServiceWorker Ready?”

What Is Progressive Enhancement?

Central to the philosophy of our app, and for any modern web app, is the principle of progressive enhancement.

Progressive enhancement means enabling as much functionality for our users as they are able to experience. It means developing sites that don’t break simply because the user’s browser doesn’t support a certain feature.

Think of progressive enhancement as a way to build your web app in a layered fashion. Begin with basic content, simple HTML links, images, and so on. Then, for your users with JavaScript support, add a layer that enhances the links to fetch the content asynchronously and replace static images of maps with interactive Google Maps. Add offline support for browsers that support service workers. Send push notifications to users who can receive them.

This not only has the advantage of showing a fully functional app to all of your users, it also makes your site more accessible to all audiences (including those using older browsers or feature phones) and allows search engines to index all of your content correctly.

When registering our service worker, we began by verifying browser support. Users with supported browsers will enjoy an enhanced experience, while the rest of our users will still get the full experience as it was before. We are progressively enhancing our app, without punishing our other users.

Don’t confuse the term progressive enhancement with progressive web apps. While progressive web apps should ideally be developed with progressive enhancement in mind, it is not a technical requirement. You can build a progressive web app that works beautifully on one modern browser, while crashing miserably in all others—please don’t.

HTTPS and Service Workers

As you just saw, service workers can intercept requests, modify their content, or even completely replace them with new responses. To protect users and prevent man-in-the-middle attacks, in which a malicious third party can take advantage of these abilities, only pages served over secure connections (HTTPS) can register a service worker.

During development, you can use service workers without a secure connection by using localhost as your hostname (for example, both http://localhost/ and http://localhost:1234/user/index.html can register and use service workers). But once you deploy your web app to your server, it must be served over a secure HTTPS connection for service workers to work.

As the web becomes more and more powerful, many of its new features require HTTPS. This does not end with service workers but is also a requirement for many other new features. Other APIs, such as SpeechRecognition, may not require HTTPS, but function much better with it. There are even some features that were previously available on nonsecure connections that have been changed to only work on HTTPS (e.g., Geolocation API).

If you need further motivation to make the move to HTTPS, Google has announced that it has begun giving a slight bonus in search rankings to pages served over a secure connection.

Serving your site on HTTPS is now cheaper and easier than ever before. A number of new certificate authorities have even started offering SSL certificates for free, and new tools and processes for configuring your server have made the process easier. If you are still clinging to HTTP, you are quickly running out of excuses.

Fetching Content from the Web

In the code we wrote earlier, we built a new response object from scratch by specifying its content and headers, and then responding to the request with it.

A much more widespread use for service workers is to respond to requests with content from the web.

Replace the code in serviceworker.js with the following:

If you followed all the previous steps, the site’s logo should have flipped upside down when you refreshed the page (Figure 2-7).

As before, we are listening to the fetch event, only this time we are looking for requests for /img/logo.png. When such a request is detected, we instead create a new request using the fetch command, passing it a URL for an alternate logo. fetch returns a promise containing a new response, which we then use to respond to the original request event using event.respondWith.

In other words, we are telling our service worker to listen for requests for our logo, request a different logo instead, and return that to the window.

Capturing Offline Requests

Let’s use everything we just learned about service workers to detect when our user is offline, and present him with a friendly error message, instead of the browser’s default error message.

We begin by modifying our serviceworker.js code to respond to all requests simply by fetching and returning the very content each request originally asked for:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request)
  );
});

Looking closely at the preceding code, you may be wondering what the point of this event is. We are listening and capturing all fetch events, only to respond to them with another fetch for exactly the same thing. If you look at the site in your browser, you will see that it indeed behaves exactly like it did before we added the service worker.

So what is the point of this? Well, you may recall that in the previous example, I mentioned that fetch returns the response wrapped in a promise. By wrapping our response with a promise, we can catch it if it is rejected and do something about it.

Replace the code in serviceworker.js with the following code:

self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return new Response(
      "Welcome to the Gotham Imperial Hotel.
"+
      "There seems to be a problem with your connection.
"+
      "We look forward to telling you about our hotel as soon as you go online."
      );
    })
  );
});

Refresh your browser to make sure the latest version of the service worker has been registered and installed, then go offline (see Simulating an Offline State) and refresh the page once more. Now, instead of the browser’s native error message, you should see a personalized message from the Gotham Imperial Hotel (Figure 2-8).

Simple offline text message
Figure 2-8. Simple offline text message

Let’s add some formatting to this message, and then go over the code line by line.

Creating HTML Responses

Since we are trying to push the web forward, not backward, let’s improve our code to send an elegant HTML page to our offline users instead of a plain-text one.

Replace the code in serviceworker.js with the following code:

var responseContent =
  "<html>" +
  "<body>" +
  "<style>" +
  "body {text-align: center; background-color: #333; color: #eee;}" +
  "</style>" +
  "<h1>Gotham Imperial Hotel</h1>" +
  "<p>There seems to be a problem with your connection.</p>" +
  "<p>Come visit us at 1 Imperial Plaza, Gotham City for free WiFi.</p>" +
  "</body>" +
  "</html>";

self.addEventListener("fetch", function(event) {
  event.respondWith(
    fetch(event.request).catch(function() {
      return new Response(
        responseContent,
        {headers: {"Content-Type": "text/html"}}
      );
    })
  );
});

Make sure you refresh your browser to register your new service worker and then visit the page while offline to see your app’s new offline message (Figure 2-9).

Simple offline HTML message
Figure 2-9. Simple offline HTML message

Let’s look at how we accomplished this.

We begin by defining the HTML content we would like to show offline users and placing it into a variable called responseContent.

We then add an event listener that listens to all fetch events. When a fetch event is detected, our function is called, receiving a FetchEvent object as an argument. We then use that event object’s respondWith method to respond to the event ourselves instead of letting its default behavior play out.

The respondWith method takes one argument, containing either a response object or code that resolves to a response object. The rest of our code builds that response.

We begin by calling fetch, and passing it the original request (found inside the FetchEvent object). We pass the original request object, not just its URL, to make sure any headers, cookies, and the request method are left untouched. fetch returns a promise. If the user is online, the server is online, the file is in the right place, and everything is right in the universe, then the promise is fulfilled, and fetch returns the response. This, in turn, gets passed back to event.respondWith, which sends it back to the page the user is browsing. If, however, something goes wrong while trying to fetch our file (e.g., the user boarded a plane), the promise is rejected, and the callback function we define inside the catch method is called.

This function constructs a response by calling new Response, passing it two arguments. The first is the body of the response (the HTML we defined earlier). The second is an optional options object. We use this options object to add a Content-Type header to the response.

This new response is then returned to our event.respondWith, which sends it back to the page as if it was a regular response returned from a web server.

Note

You may be wondering, why we had to define the Content-Type header manually when creating our response. Try to modify the code to simply return the response content without this header, and see what happens.

The browser treats the response as if it was plain text. Everything gets displayed as plain text, including HTML tags and styles.

You don’t usually need to tell the browser that HTML is HTML, so what is going on here?

Most web servers are configured to automatically serve most common file types with the correct headers automatically. When a server sends an HTML file, it constructs a response that contains both the HTML, as well as many headers, including a Content-Type header letting the browser know what to do with the response. Because we are constructing a response from scratch, it is up to us to not only worry about the response’s content (the HTML) but also its headers.

Understanding Service Worker Scope

Earlier in this chapter we placed the service worker’s file in the root directory of our project. Let’s explore why it was important to place this file there, and not in a subdirectory (e.g., /js/sw.js).

Because service workers are so powerful and can modify any request that passes through them, certain security restrictions need to be placed on them.

Imagine for a minute that you have a site that lists the best restaurants in Gotham (e.g., www.GothamEats.com). Now imagine that you allowed each restaurant to host a site under your domain offering its menu and photos (e.g., www.GothamEats.com/Ginnos). What would happen if the owner of Ginno’s uploaded a service worker script to his site (e.g., www.GothamEats.com/Ginnos/sw.js) that would change all traffic to his competitor’s site (e.g., www.GothamEats.com/Ralphs) to say it was out of business? The day our browsers allow this to happen is not going to be a pleasant day in Gotham.

To prevent this issue, each service worker has a limited scope it can control. This scope is defined by the directory where the service worker’s JavaScript file is placed in. By placing our serviceworker.js file in the root directory of our project earlier, we allow it to control all requests that originate anywhere in the site. If we had placed it in the js directory, only requests that originated from that subdirectory would have passed through it.

You can overwrite a service worker’s scope by passing it a scope option when registering it. This can limit the service worker’s scope to a smaller subset of directories, but it cannot extend the scope to a wider scope than would otherwise be available to it (e.g., you can limit a service worker located at /ginnos/sw.js to only affect requests to /ginnos/menu/, but you can’t extend its scope to the root of the domain).

// These two commands will have the exact same scope:
navigator.serviceWorker.register("/sw.js");
navigator.serviceWorker.register("/sw.js", {scope: "/"});

// These two commands will register two service workers
// each controlling a different directory:
navigator.serviceWorker.register("/sw-ginnos.js", {scope: "/Ginnos"});
navigator.serviceWorker.register("/sw-ralphs.js", {scope: "/Ralphs"});

Summary

While it is easy to dismiss what we’ve accomplished as simply replacing the browser’s error message with a slightly less fancy error message,2 we have actually accomplished an incredible feat here.

By registering a service worker, and listening to requests, we placed ourselves between the browser window and the network. We learned how to intercept every request made by the page (including to third-party servers), and how to change them, replace them, or detect when they fail.

Most importantly, we enhanced our site’s functionality so that offline users are no longer left in the dark. Users arriving in Gotham City (where cell towers tend to mysteriously go up in flames every other day) can browse a simplified version of our site even while offline.

In the next chapter, we will take what we have just learned about the service worker, sprinkle on some caching magic, and provide the user with the full Gotham Imperial Hotel experience whether they are online or offline.

1 If you do not see this error message, make sure your local server is running, and read about browser support Browser Support for Service Workers.

2 Did you know you can play with the dinosaur on Chrome’s error page? Go ahead and click it—that is fancy!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset