Build a PWA using Workbox

Workbox is the successor to sw-precache and sw-toolbox. It is a collection of libraries and tools used for generating a service worker, and for 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

  • How to write a service worker using the workbox-sw.js library
  • How to add routes to your service worker using workbox-sw.js
  • How to use the predefined caching strategies provided in workbox-sw.js
  • How to augment the workbox-sw.js caching strategies with custom logic
  • How to generate a production-grade service worker with workbox-cli

What you should already know

  • Basic HTML, CSS, and JavaScript
  • ES2015 promises
  • How to run commands from the command line
  • Some familiarity with service workers is recommended
  • Familiarity with Node.js is recommended

What you will need

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

Clone the starter code from GitHub using 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

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 you 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 you can use it directly from the command line, which you'll do in a later step.

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

  • The build script uses the copyfiles package to copy the files from the src folder to the build folder. This is currently our app's "build" process.
  • The start script starts an express server inside the build folder to serve our app.

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'll be building the lab.

This folder contains:

  • src/images is a folder that contains sample images
  • src/js/animation.js is a javascript file for page animations
  • src/pages is a folder that contains sample pages
  • src/style/main.css is the stylesheet for the app's pages
  • src/sw.js is where you will write your source service worker using workbox-sw.js
  • src/index.html is the home page HTML file
  • package.json and package-lock.json track Node.js dependencies
  • server.js is a simple express server

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 you. Using a tool like workbox-cli has multiple advantages:

  1. The tool can be integrated into your build process. Adding workbox-cli to your build process eliminates the need for manual updates to the precache manifest each time that you 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 you 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" and 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 this for now (if you already tried it, that's okay). Rather than building your service worker directly, you 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 modifying the config file in a 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 now 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 you can test your 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 your production service worker file (build/sw.js). Because you configured workbox-config.js to cache *.css files, your 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 you update our app, you can simply 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 your 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 your home page still loads!

Explanation

By editing the globPatterns files in workbox-config.js, you can easily update the manifest and precached files. Re-running the workbox injectManifest command (via npm run build) updates your 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 you were offline even though you 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 your 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 users' 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 five 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 on 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 revisit 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 you are using the networkFirst strategy to handle a resource you expect to update frequently (such as 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. In the caching strategy, you could have passed directly to the second argument of registerRoute as you did in the previous examples. Instead, you return a call to the handle method in a custom handler function to gain access to the response, as you'll see in the next step.

Handle invalid responses

The handle method returns a promise that resolves with a response, so you 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 the following:

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 the following:

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 your custom 404 page is returned. Otherwise, you 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 add a final service worker route for the app's "post" pages (pages/post1.html, pages/post2.html, etc.).

The route should use Workbox's cacheFirst strategy, creating a cache called "posts-cache" that stores a maximum of 50 entries.

If the cache or network returns a resource with a 404 status, then return the pages/404.html resource.

If the resource is not available in the cache, and the app is offline, then return the pages/offline.html resource.

Hints

  • To test your changes, remember to restart the server with npm run start and rebuild the service worker with npm run build.
  • Review the previous section for how to add additional logic to Workbox's built-in strategy handlers.
  • The Workbox cacheFirst handler handles no-connectivity differently than networkFirst. Instead of passing an empty response to the .then, it rejects and goes to the next .catch.

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

What we've covered

  • Writing a service worker using the workbox-sw.js library
  • Adding routes to your service worker using workbox-sw.js
  • Using the predefined caching strategies provided in workbox-sw.js
  • Augmenting the workbox-sw.js caching strategies with custom logic
  • Generating a production-grade service worker with workbox-cli

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