Don't miss out on the action at this year's Chrome Dev Summit, streaming live on YouTube. Watch now.

Lab: Scripting the Service Worker

Concepts: Introduction to Service Worker

Overview

This lab walks you through creating a simple service worker.

What you will learn

  • Create a basic service worker script, install it, and do simple debugging

What you should know

What you need before you begin

1. Get set up

If you have not downloaded the repository, installed Node, and started a local server, follow the instructions in Setting up the labs.

Open your browser and navigate to localhost:8080/service-worker-lab/app.

If you have a text editor that lets you open a project, open the service-worker/app folder. This will make it easier to stay organized. Otherwise, open the folder in your computer's file system. The app folder is where you will be building the lab.

This folder contains:

  • other.html, js/other.js, below/another.html, and js/another.js are sample resources that we use to experiment
  • index.html is the main HTML page for our sample site/application
  • index.css is the cascading stylesheet for index.html
  • service-worker.js is the JavaScript file that is used to create our service worker
  • styles folder contains the cascading stylesheets for this lab
  • test folder contains files for testing your progress

2. Register the service worker

Open service-worker.js in your text editor. Note that the file contains only an empty function. We have not added any code to run within the service worker yet.

Open index.html in your text editor.

Replace TODO 2 with the following code:

index.html

if (!('serviceWorker' in navigator)) {
  console.log('Service worker not supported');
  return;
}
navigator.serviceWorker.register('service-worker.js')
.then(function() {
  console.log('Registered');
})
.catch(function(error) {
  console.log('Registration failed:', error);
});

Save the script and refresh the page. The console should return a message indicating that the service worker was registered.

In your browser, navigate to test-registered.html (app/test/test-registered.html) to confirm that you have registered the service worker. This is a unit test. Passed tests are blue and failed tests are red. If you've done everything correctly so far, this test should be blue. Close the test page when you are done with it.

Optional: Open the site on an unsupported browser and verify that the support check conditional works.

Explanation

Service workers must be registered. Always begin by checking whether the browser supports service workers. The service worker is exposed on the window's Navigator object and can be accessed with window.navigator.serviceWorker.

In our code, if service workers aren't supported, the script logs a message and fails immediately. Calling serviceworker.register(...) registers the service worker, installing the service worker's script. This returns a promise that resolves once the service worker is successfully registered. If the registration fails, the promise will reject.

3. Listening for life cycle events

Changes in the service worker's status trigger events in the service worker.

3.1 Add event listeners

Open service-worker.js in your text editor.

Replace TODO 3.1 with the following code:

service-worker.js

self.addEventListener('install', function(event) {
  console.log('Service worker installing...');
  // TODO 3.4: Skip waiting
});

self.addEventListener('activate', function(event) {
  console.log('Service worker activating...');
});

Save the file. Close app/test/test-registered.html page if you have not already. Manually unregister the service worker and refresh the page to install and activate the updated service worker. The console log should indicate that the new service worker was registered, installed, and activated.

Explanation

The service worker emits an install event at the end of registration. In this case we log a message, but this is a good place for caching static assets.

When a service worker is registered, the browser detects if the service worker is new (either because it is different from the previously installed service worker or because there is no registered service worker for this site). If the service worker is new (as it is in this case) then the browser installs it.

The service worker emits an activate event when it takes control of the page. We log a message here, but this event is often used to update caches.

Only one service worker can be active at a time for a given scope (see Exploring service worker scope), so a newly installed service worker isn't activated until the existing service worker is no longer in use. This is why all pages controlled by a service worker must be closed before a new service worker can take over. Since we unregistered the existing service worker, the new service worker was activated immediately.

3.2 Re-register the existing service worker

Reload the page. Notice how the events change.

Now close and reopen the page (remember to close all pages associated with the service worker). Observe the logged events.

Explanation

After initial installation and activation, re-registering an existing worker does not re-install or re-activate the service worker. Service workers also persist across browsing sessions.

3.3 Update the service worker

Replace TODO 3.3 in service-worker.js with the following comment:

service-worker.js

// I'm a new service worker

Save the file and refresh the page. Notice that the new service worker installs but does not activate.

Navigate to test-waiting.html (app/test/test-waiting.html) to confirm that the new service worker is installed but not activated. The test should be passing (blue).

Close all pages associated with the service worker (including the app/test/test-waiting.html page). Reopen the app/ page. The console log should indicate that the new service worker has now activated.

Explanation

The browser detects a byte difference between the new and existing service worker file (because of the added comment), so the new service worker is installed. Since only one service worker can be active at a time (for a given scope), even though the new service worker is installed, it isn't activated until the existing service worker is no longer in use. By closing all pages under the old service worker's control, we are able to activate the new service worker.

3.4 Skipping the waiting phase

It is possible for a new service worker to activate immediately, even if an existing service worker is present, by skipping the waiting phase.

Replace TODO 3.4 in service-worker.js with the following code:

service-worker.js

self.skipWaiting();

Save the file and refresh the page. Notice that the new service worker installs and activates immediately, even though a previous service worker was in control.

Explanation

The skipWaiting() method allows a service worker to activate as soon as it finishes installation. The install event listener is a common place to put the skipWaiting() call, but it can be called anywhere during or before the waiting phase. See this documentation for more on when and how to use skipWaiting(). For the rest of the lab, we can now test new service worker code without manually unregistering the service worker.

For more information

4. Intercept network requests

Service Workers can act as a proxy between your web app and the network.

Replace TODO 4 in service-worker.js with:

service-worker.js

self.addEventListener('fetch', function(event) {
  console.log('Fetching:', event.request.url);
});

Save the script and refresh the page to install and activate the updated service worker.

Check the console and observe that no fetch events were logged. Refresh the page and check the console again. You should see fetch events this time for the page and its assets (like CSS).

Click the links to Other page, Another page, and Back.

You'll see fetch events in the console for each of the pages and their assets. Do all the logs make sense?

Explanation

The service worker receives a fetch event for every HTTP request made by the browser. The fetch event object contains the request. Listening for fetch events in the service worker is similar to listening to click events in the DOM. In our code, when a fetch event occurs, we log the requested URL to the console (in practice we could also create and return our own custom response with arbitrary resources).

Why didn't any fetch events log on the first refresh? By default, fetch events from a page won't go through a service worker unless the page request itself went through a service worker. This ensures consistency in your site; if a page loads without the service worker, so do its subresources.

For more information

Solution code

To get a copy of the working code, navigate to the 04-intercepting-network-requests folder.

5. Optional: Exploring service worker scope

Service workers have scope. The scope of the service worker determines from which paths the service worker intercepts requests.

5.1 Find the scope

Update the registration code in index.html with:

index.html

if (!('serviceWorker' in navigator)) {
  console.log('Service worker not supported');
  return;
}
navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {
  console.log('Registered at scope:', registration.scope);
})
.catch(function(error) {
  console.log('Registration failed:', error);
});

Refresh the browser. Notice that the console shows the scope of the service worker (for example http://localhost:8080/service-worker-lab/app/).

Explanation

The promise returned by register() resolves to the registration object, which contains the service worker's scope.

The default scope is the path to the service worker file, and extends to all lower directories. So a service worker in the root directory of an app controls requests from all files in the app.

5.2 Move the service worker

Unregister the current service worker.

Then move service-worker.js into the app/below directory and update the service worker URL in the registration code. Unregister the service worker and refresh the page.

The console shows that the scope of the service worker is now localhost:8080/service-worker-lab/app/below/.

Navigate to test-scoped.html (app/test/test-scoped.html) to confirm that that service worker is registered in app/below/. If you've done everything correctly, you shouldn't see any red errors. Close the test page when you are done with it.

Back on the main page, click Other page, Another page and Back. Which fetch requests are being logged? Which aren't?

Explanation

The service worker's default scope is the path to the service worker file. Since the service worker file is now in app/below/, that is its scope. The console is now only logging fetch events for another.html, another.css, and another.js, because these are the only resources within the service worker's scope (app/below/).

5.3 Set an arbitrary scope

Unregister the current service worker again.

Move the service worker back out into the project root directory (app) and update the service worker URL in the registration code.

Use the reference on MDN to set the scope of the service worker to the app/below/ directory using the optional parameter in register(). Unregister the service worker and refresh the page. Click Other page, Another page and Back.

Again the console shows that the scope of the service worker is now localhost:8080/service-worker-lab/app/below, and logs fetch events only for another.html, another.css, and another.js.

Navigate to test-scoped.html again to confirm that the service worker is registered in app/below/.

Explanation

It is possible to set an arbitrary scope by passing in an additional parameter when registering, for example:

index.html

navigator.serviceWorker.register('/service-worker.js', {
  scope: '/kitten/'
});

In the above example the scope of the service worker is set to /kitten/. The service worker intercepts requests from pages in /kitten/ and /kitten/lower/ but not from pages like /kitten or /.

For more information

Solution code

To get a copy of the working code, navigate to the solution folder.

Congratulations!

You now have a simple service worker up and running.