Workbox is the successor to sw-precache and sw-toolbox. It is a collection of libraries and tools used for generating a service worker, precaching, routing, and runtime-caching. Workbox also includes modules for easily integrating background sync and Google Analytics into your service worker.

In this lab, you'll use the workbox-sw.js library and the workbox-cli Node.js module to build an offline-capable progressive web app (PWA).

What you'll learn

What you should already know

What you will need

This lab requires Node.js. Install the latest long term support (LTS) version if you have not already.

Clone the starter code from GitHub with the following command:

git clone https://github.com/googlecodelabs/workbox-lab.git

Alternatively, you can click here to download the code as a zip file.

Navigate to the project directory via the command line:

cd workbox-lab/project/

Run the following commands to install the project dependencies:

npm install
npm install --global workbox-cli

Then build and serve the app with these commands:

npm run build
npm run start

Explanation

The npm install command installs the project dependencies based on the configuration in package.json. Open project/package.json and examine its contents.

workbox-cli is a command-line tool that lets us configure, generate, and modify service worker files. You'll learn more about this in a later step.

workbox-cli is installed globally (with the --global flag) so that we can use it directly from the command line, which we do in a later step.

The remainder of package.json is used to configure the following:

Open the app and explore the code

Once you have started the server, open the browser and navigate to http://localhost:8081/ to view the app. The app is a news site containing some "trending articles" and "archived posts". We will be performing different runtime caching strategies based on whether the request is for a trending article or archived post.

Open the workbox-lab/project folder in your text editor. The project folder is where you will be building the lab.

This folder contains:

All of these files were copied over to the build folder when the npm run build command was run, and the server (started with npm run start) is serving these files from the build directory.

Observe that we currently have an empty service worker (build/sw.js) which was installed in the browser by the registration code in index.html (you can see the status of service workers in Chrome DevTools by clicking on Service Workers in the Application tab):

index.html

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/sw.js')
        .then(registration => {
          console.log(`Service Worker registered! Scope: ${registration.scope}`);
        })
        .catch(err => {
          console.log(`Service Worker registration failed: ${err}`);
        });
    });
  }
</script>

Now that we have the starting app working, let's start writing the service worker.

In the empty source service worker file, src/sw.js, add the following snippet:

src/sw.js

importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js');

if (workbox) {
  console.log(`Yay! Workbox is loaded 🎉`);

  workbox.precaching.precacheAndRoute([]);

} else {
  console.log(`Boo! Workbox didn't load 😬`);
}

Save the file.

Explanation

In this code, the importScripts call imports the workbox-sw.js library from a Content Delivery Network (CDN). Once the library is loaded, the workbox object gives our service worker access to all the Workbox modules.

The precacheAndRoute method of the precaching module takes a precache "manifest" (a list of file URLs with "revision hashes") to cache on service worker installation. It also sets up a cache-first strategy for the specified resources, serving them from the cache by default.

Currently, the array is empty, so no files will be cached.

Rather than adding files to the list manually, workbox-cli can generate the manifest for us. Using a tool like workbox-cli has multiple advantages:

  1. The tool can be integrated into our build process. Adding workbox-cli to our build process eliminates the need for manual updates to the precache manifest each time that we update the app's files.
  2. workbox-cli automatically adds "revision hashes" to the files in the manifest entries. The revision hashes enable Workbox to intelligently track when files have been modified or are outdated, and automatically keep caches up to date with the latest file versions. Workbox can also remove cached files that are no longer in the manifest, keeping the amount of data stored on a user's device to a minimum. You'll see what workbox-cli and the file revision hashes look like in the next section.

Learn more

While we are using workbox-cli in this lab, Workbox also supports tools like gulp with workbox-build and webpack with workbox-webpack-plugin.

The first step towards injecting a precache manifest into the service worker is configuring which files we want to precache. In this step we create the workbox-cli configuration file.

From the project directory, run the following command:

workbox wizard --injectManifest

Next, follow the command-line prompts as described below:

  1. The first prompt asks for the root of the app. The root specifies the path where Workbox can find the files to cache. For this lab, the root is the build/ directory, which should be suggested by the prompt. You can either type "build/" or choose "build/" from the list.
  2. The second prompt asks what types of files to cache. For now, choose to cache CSS files only.
  3. The third prompt asks for the path to your source service worker. This is the service worker file, src/sw.js, to which we added code in the previous step. Type "src/sw.js" and press return.
  4. The fourth prompt asks for a path in which to write the production service worker. You can type "build/sw.js" or press return.
  5. The final prompt asks what we want to name our configuration file. Press return and use the default answer (workbox-config.js).

Once you've completed the prompts, you'll see a log with instructions for building the service worker. Ignore that for now (if you already tried it, that's okay). Rather than building our service worker directly, we will add it to the build process in the next step.

But first, examine the newly created workbox-config.js file.

Explanation

Workbox creates a configuration file (in this case workbox-config.js) that workbox-cli uses to generate service workers. The config file specifies where to look for files (globDirectory), which files to precache (globPatterns), and the file names for our source and production service workers (swSrc and swDest, respectively). We can also modify this config file directly to change what files are precached. We explore that in the later step.

Now let's use the workbox-cli tool to inject the precache manifest into the service worker.

Open package.json and update the build script to run the Workbox injectManifest command. The updated package.json should look like the following:

package.json

{
  "name": "workbox-lab",
  "version": "1.0.0",
  "description": "a lab for learning workbox",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "copy": "copyfiles -u 1 src/**/**/* src/**/* src/* build",
    "build": "npm run copy && workbox injectManifest workbox-config.js",
    "start": "node server.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.3"
  },
  "devDependencies": {
    "copyfiles": "^1.2.0",
    "workbox-cli": "^3.5.0"
  }
}

Save the file and run npm run build from the command line.

The precacheAndRoute call in build/sw.js has been updated. In your text editor, open build/sw.js and observe that style/main.css is included in the file manifest.

Return to the app in your browser (http://localhost:8081/). Open your browser's developer tools (in Chrome use Ctrl+Shift+I on Windows, Cmd+Opt+I on Mac). Unregister the previous service worker and clear all service worker caches for localhost so that we can test our new service worker. In Chrome DevTools, you can do this in one easy operation by going to the Application tab, clicking Clear Storage and then clicking the Clear site data button.

Refresh the page and check that a new service worker was installed. You can see your service workers in Chrome DevTools by clicking on Service Workers in the Application tab. Check the cache and observe that main.css is stored. In Chrome DevTools, you can see your caches by clicking on Cache Storage in the Application tab.

Explanation

When workbox injectManifest is called, Workbox makes a copy of the source service worker file (src/sw.js) and injects a manifest into it, creating our production service worker file (build/sw.js). Because we configured workbox-config.js to cache *.css files, our production service worker has style/main.css in the manifest. As a result, style/main.css was pre-cached during the service worker installation.

Now whenever we update our app, we can simple run npm run build to rebuild the app and update the service worker.

Let's modify the Workbox config file to precache our entire home page. Replace the contents of workbox-config.js with the following code, and save the file:

workbox-config.js

module.exports = {
  "globDirectory": "build/",
  "globPatterns": [
    "**/*.css",
    "index.html",
    "js/animation.js",
    "images/home/*.jpg",
    "images/icon/*.svg"
  ],
  "swSrc": "src/sw.js",
  "swDest": "build/sw.js",
  "globIgnores": [
    "../workbox-config.js"
  ]
};

From the project directory, re-run npm run build to update build/sw.js. The precache manifest in the production service worker (build/sw.js) has been updated to contain index.html, main.css, business.jpg, animation.js, and icon.svg.

Refresh the app and activate the updated service worker in the browser. In Chrome DevTools, you can activate the new service worker by going to the Application tab, clicking Service Workers and then clicking skipWaiting. Observe in developer tools that the globPatterns files are now in the cache (you might need to refresh the cache to see the new additions).

Return to the command line window that is running our server (the one started with npm run start) and turn off the server by pressing Ctrl+C. Now our app is "offline". Refresh the page and observe that our home page still loads!

Explanation

By editing the globPatterns files in workbox-config.js, we can easily update the manifest and precached files. Re-running the workbox injectManifest command (via npm run build) updates our production service worker with the new configuration.

In addition to precaching, the precacheAndRoute method sets up an implicit cache-first handler. This is why the home page loaded while we were offline even though we had not written a fetch handler for those files!

workbox-sw.js has a routing module that lets you easily add routes to your service worker.

Let's add a route to the service worker now. Copy the following code into src/sw.js beneath the precacheAndRoute call. Make sure you're not editing the production service worker, build/sw.js, as this file will be overwritten when we run workbox injectManifest again.

src/sw.js

workbox.routing.registerRoute(
  /(.*)articles(.*)\.(?:png|gif|jpg)/,
  workbox.strategies.cacheFirst({
    cacheName: 'images-cache',
    plugins: [
      new workbox.expiration.Plugin({
        maxEntries: 50,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
      })
    ]
  })
);

Save the file.

Restart the server and rebuild the app and service worker with the following commands:

npm run build
npm run start

Refresh the app and activate the updated service worker in the browser. Navigate to Article 1 and Article 2 . Check the caches to see that the images-cache now exists and contains the images from Articles 1 and 2. You may need to refresh the caches in developer tools to see the contents.

Explanation

In this code we added a route to the service worker using the registerRoute method on the routing class. The first parameter in registerRoute is a regular expression URL pattern to match requests against. The second parameter is the handler that provides a response if the route matches. In this case the route uses the strategies class to access the cacheFirst run-time caching strategy. Whenever our app requests article images, the service worker checks the cache first for the resource before going to the network.

The handler in this code also configures Workbox to maintain a maximum of 50 images in the cache (ensuring that user's devices don't get filled with excessive images). Once 50 images has been reached, Workbox will remove the oldest image automatically. The images are also set to expire after 30 days, signaling to the service worker that the network should be used for those images.

Optional: Write your own route that caches the user avatar. The route should match requests to /images/icon/* and handle the request/response using the staleWhileRevalidate strategy. Give the cache the name "icon-cache" and allow a maximum of 5 entries to be stored in the cache. This strategy is good for icons and user avatars that change frequently but the latest versions are not essential to the user experience. You'll need to remove the icon from the precache manifest so that the service worker uses your staleWhileRevalidate route instead of the implicit cache-first route established by the precache method.

Write the basic handler using the handle method

Sometimes content must always be kept up-to-date (e.g., news articles, stock figures, etc.). For this kind of data, the cacheFirst strategy is not the best solution. Instead, we can use the networkFirst strategy to fetch the newest content first, and only if that fails does the service worker get old content from the cache.

Add the following code below the previous route in src/sw.js:

src/sw.js

const articleHandler = workbox.strategies.networkFirst({
  cacheName: 'articles-cache',
  plugins: [
    new workbox.expiration.Plugin({
      maxEntries: 50,
    })
  ]
});

workbox.routing.registerRoute(/(.*)article(.*)\.html/, args => {
  return articleHandler.handle(args);
});

Save the file and run npm run build in the command-line to rebuild the service worker. Clear the caches and then update the service worker in the browser. In Chrome's developer tools, you can clear the cache and unregister the service worker simultaneously from the Application tab by going to Clear storage and clicking Clear site data. Refresh the home page and click a link to one of the Trending Articles. Check the caches to see that the articles-cache was created and contains the article you just clicked. You may need to refresh the caches to see the changes.

Optional: Test that articles are cached dynamically by visiting some while online. Then take the app offline again by pressing Ctrl+C in the command line and re-visit those articles. Instead of the browser's default offline page, you should see the cached article. Remember to re-run npm run start to restart the server.

Optional: Test the networkFirst strategy by changing the text of the cached article and reloading the page. Make sure to run npm run build to update the build files. Even though the old article is cached, the new one is served and the cache is updated.

Explanation

Here we are using the networkFirst strategy to handle a resource we expect to update frequently (trending news articles). This strategy updates the cache with the newest content each time it's fetched from the network.

In the above code we use the handle method on the built-in networkFirst strategy. The handle method takes the object passed to the handler function (in this case we called it args) and returns a promise that resolves with a response. We could have passed in the caching strategy directly to the second argument of registerRoute as we did in the previous examples, but we return a call to the handle method in a custom handler function instead to gain access to the response, as you'll see in the next step.

Handle invalid responses

The handle method returns a promise resolving with the response, so we can access the response with a .then.

Add the following .then inside the article route after the call to the handle method:

src/sw.js

.then(response => {
    if (!response) {
      return caches.match('pages/offline.html');
    } else if (response.status === 404) {
      return caches.match('pages/404.html');
    }
    return response;
  });

The updated route should look like:

src/sw.js

workbox.routing.registerRoute(/(.*)article(.*)\.html/, args => {
  return articleHandler.handle(args).then(response => {
    if (!response) {
      return caches.match('pages/offline.html');
    } else if (response.status === 404) {
      return caches.match('pages/404.html');
    }
    return response;
  });
});

Then add pages/offline.html and pages/404.html to the workbox-config.js file for precaching. The full file should look like this:

workbox-config.js

module.exports = {
  "globDirectory": "build/",
  "globPatterns": [
    "**/*.css",
    "index.html",
    "js/animation.js",
    "images/home/*.jpg",
    "images/icon/*.svg",
    "pages/offline.html",
    "pages/404.html"
  ],
  "swSrc": "src/sw.js",
  "swDest": "build/sw.js",
  "globIgnores": [
    "../workbox-config.js"
  ]
};

Save the files and run npm run build in the command line. Clear the caches and then activate the updated service worker in the browser. On the app home page, try clicking the Non-existent article link. This link points to an HTML page, pages/article-missing.html, that doesn't actually exist. You should see the custom 404 page that we precached!

Now try taking the app offline by pressing Ctrl+C in the command line and then click any of the links to the articles. You should see the custom offline page!

Explanation

The .then statement receives the response passed in from the handle method. If the response doesn't exist, then it means the user is offline and the response was not previously cached. Instead of letting the browser show a default offline page, the service worker returns the custom offline page that was precached. If the response exists but the status is 404, then our custom 404 page is returned. Otherwise, we return the original response.

So far you have implemented many caching strategies with Workbox, and in the previous section you learned how to add custom logic to the default Workbox caching options.

This last exercise is a challenge with less guidance (you can still see the solution code if you get stuck). You need to:

Hints

You have learned how to use Workbox to create production-ready service workers!

What we've covered

You can also use Workbox with build tools like gulp and webpack! To learn how to use the workbox-build library with gulp and webpack, check out this other workbox lab.

Resources