Adding a Workbox Service Worker to an Angular project

Published: June 22, 2018  •  pwa, javascript

When you bootstrapped an Angular application with the CLI (ng new projectname), it is very easy to add a Service Worker to the project. Issue the following command and you have a fully functional Service Worker.

ng add  @angular/pwa --project projectname

Make sure that projectname matches the name of the project in angular.json.

The command also creates the configuration file ngsw-config.json in the root of your project, that allows you to fine tune the Service Worker. The default configuration is set up to cache index.html, favicon.ico, all build artifacts (JavaScript and CSS) and anything under assets. For more information visit the official Angular Service Worker documentation.


This is a great solution, but for another blog post I needed to write some custom Service Worker code and I couldn't figure out how to add my code to the Angular Service Worker. (Send me a message if there is an easy way to do that)

Fortunately, it is not that complicated to add a custom Service Worker to an Angular project. Here is the way I did it.

Create a JavaScript file src/service-worker.js, that contains the Service Worker code, and the manifest file src/manifest.json.

To create the manifest file I used this online generator: https://app-manifest.firebaseapp.com/
This web application also automatically creates icons in different sizes, based on the 512x512 image you have to provide.

manifest generator

{
  "name": "ngworkbox",
  "short_name": "ngworkbox",
  "theme_color": "#2196f3",
  "background_color": "#2196f3",
  "display": "standalone",
  "Scope": "/",
  "start_url": "/index.html",
  "icons": [
    {
      "src": "assets/images/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "assets/images/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "assets/images/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "assets/images/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "assets/images/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "assets/images/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "assets/images/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "assets/images/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "splash_pages": null
}

manifest.json


Next we have to tell Angular about these two new files. Open angular.json and add two entries to the architect/build/options/assets configuration:

      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/ngworkbox",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/assets",
              "src/manifest.json",      <--------
              "src/service-worker.js"   <--------
            ],

Open src/index.html and add the JavaScript code that loads the Service Worker.

  <script>
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('service-worker.js').then(function (registration) {
        console.log('Service Worker registration successful with scope: ', registration.scope);
      }, function (err) {
        console.log('Service Worker registration failed: ', err);
      });

      navigator.serviceWorker.ready.then(function (registration) {
        console.log('Service Worker ready');
      });
    }
  </script>

index.html


Workbox

This is the basic setup you need for a custom Service Worker. The more difficult part is to write the code. That's the reason projects like the Angular Service Worker exists, to simplify the development. Fortunately, when you write your own Service Worker you don't have to write everything from scratch, you can use a library. My preferred library for writing Service Worker code is Workbox from Google.

To add Workbox to our project we first install the library and the CLI with npm

npm install workbox-sw
npm install workbox-cli -D

Next we have to instruct the Angular build system to copy the Workbox files to the dist folder when it builds the project. For that we add the following entries in the angular.json file to the architect/build/options/assets array:

              "src/manifest.json",
              "src/service-worker.js",
              {
                "glob": "workbox-sw.js",
                "input": "node_modules/workbox-sw/build",
                "output": "./workbox-3.3.1"
              },
              {
                "glob": "workbox-core.prod.js",
                "input": "node_modules/workbox-core/build/",
                "output": "./workbox-3.3.1"
              },
              {
                "glob": "workbox-precaching.prod.js",
                "input": "node_modules/workbox-precaching/build/",
                "output": "./workbox-3.3.1"
              }

angular.json

This example only depends on the core and precaching module.


In the file src/service-worker.js we add this code

importScripts('workbox-3.3.1/workbox-sw.js');
workbox.setConfig({
  debug: false,
  modulePathPrefix: 'workbox-3.3.1/'
});
workbox.skipWaiting();
workbox.clientsClaim();
workbox.precaching.precacheAndRoute([]);


service-worker.js

Workbox is a modular library and loads required modules on demand. The Angular build system creates a subdirectory workbox-3.4.1 and copies all the Workbox files there. Because that's not the default location, we have to tell Workbox with modulePathPrefix where to find the modules.

The line workbox.precaching.precacheAndRoute([]); caches the assets and installs routes to them, which then makes sure that we can work with the application when the device is offline. You might be wondering why we pass an empty argument to the method, shouldn't we pass a list of the resources. That's correct, that's what we have to do. But we don't know the names of the resources until the application is built by the Angular CLI. The Angular build system creates versioned file names with a hash in their names, so the filenames will be different each time we do a production build of our application.

Fortunately this is not a problem, because Workbox has a command line tool that injects the filenames into the Service Worker code. The injection point is the text precacheAndRoute([]). When the command runs it looks for this string and adds the resources between [ and ].

To run the Workbox CLI we create a new entry to the scripts section in the package.json file. We have to make sure that the Workbox CLI runs after the Angular CLI built the application.

    "dist": "ng build --prod && workbox injectManifest",

package.json

injectManifest is the command that inserts the resources into the Service Worker code.

To start a production build of the application we now have to issue npm run dist instead of just ng build --prod.


As the last step we have to create a configuration file for the Workbox CLI. Create the file workbox-config.js in the root of your project and add this code:

module.exports = {
  "globDirectory": "dist/ngworkbox/",
  "globPatterns": [
    "index.html",
  "*.js",
  "*.css",    
    "assets/**/*.png"
  ],
  "dontCacheBustUrlsMatching": new RegExp('.+\.[a-f0-9]{20}\..+'),
  "maximumFileSizeToCacheInBytes": 5000000,
  "swSrc": "src/service-worker.js",
  "swDest": "dist/ngworkbox/service-worker.js"
};



workbox-config.js

This configuration tells Workbox what files it should cache and where to find them. My project in this example is named ngworkbox. The configuration will cache index.html, all files ending with .js and .css and every png file in the assets folder and subfolders, in the dist/ngworkbox directory. swSrc and swDest define the Service Worker source and target locations. The Workbox CLI does not change the source Service Worker, only the Service Worker in swDest will be recreated each time the build runs.

The Workbox CLI automatically adds a cache buster to the resources, but because the Angular build system creates versioned file names we don't have to add the cache buster to these resources. That's what we tell the CLI with the dontCacheBustUrlsMatching option. The regular expression looks for a 20 character hex string in the filename. If the regular expression matches, the CLI does not add the cache buster.

You can compare source and build artifact after a successful npm run dist run.

Source: src/service-worker.js

workbox.precaching.precacheAndRoute([]);

Build: dist/ngworkbox/service-worker.js

workbox.precaching.precacheAndRoute([
  {
    "url": "index.html",
    "revision": "c3adeb6914ea50792260a083e4d40938"
  },
  {
    "url": "main.66fcb64e971702bfa72e.js"
  },
  {
    "url": "polyfills.2f4a59095805af02bd79.js"
  },
  {
    "url": "runtime.a66f828dca56eeb90e02.js"
  },
  {
    "url": "styles.34c57ab7888ec1573f9c.css"
  },
  {
    "url": "assets/images/icons/icon-128x128.png",
    "revision": "8225f6e6dd0f79dc11a6348eed06430c"
  },
  ...

You see that the Workbox CLI added the cache buster (revision) only to the resources with a static file name.


That's all you need for adding Workbox to an Angular project. We have a fully functional Service Worker that automatically caches all the resources for us, and we have still the ability to add custom code to the Service Worker.


To test the production build locally you must use a separate HTTP server. You can use any HTTP server, for this project I use the http-server package.

npm install http-server -D

You can then either start it with npx

npx http-server dist/ngworkbox -c-1 -o -a localhost -p 1234

or add a script to package.json

"open": "http-server dist/ngworkbox -c-1 -o -a localhost -p 1234"

and then start it with

npm run open

In both examples the http server listens on port 1234 with disabled caching (-c-1) and it automatically opens the browser (-o).