Angular 5 and the Angular CLI provide improved support for Progressive Web Applications (PWA).

Background

I am currently working on a PWA built using version 3 of the Ionic Framework:

Step 1 - Update the Angular CLI

To update the global and local (devDependencies) versions of the Angular CLI we'll use the Node Package Manager (npm):

npm install -g @angular/cli
npm install @angular/cli --save-dev

You can check the version by running the following command:

ng --version

You should see output like:

Angular CLI: 1.7.4
Node: 9.9.0
OS: darwin x64
Angular: 5.0.3
...   

Step 2 - Adding a Service Worker

Angular's support for Service Workers is provided by the @angular/service-worker module.

You can add Angular's Service Worker (NGSW) module to your Ionic project using npm:

npm install @angular/service-worker --save

core.module.ts

Next we need to update our Core Module. We'll start by importing the ServiceWorkerModule from the @angular/service-worker package:

...

import { ServiceWorkerModule } from '@angular/service-worker';

import { AngularFireModule } from 'angularfire2';
import { AngularFirestore, AngularFirestoreModule } from 'angularfire2/firestore';
import { AngularFireAuthModule } from 'angularfire2/auth';

import { ENV } from '@env';

...

Then we need to add the ServiceWorkerModule to the Core Module's imports array:

  @NgModule({
    imports: [
      CommonModule,
      IonicModule,
      AngularFireModule.initializeApp(ENV.firebase),
      AngularFirestoreModule,
      // ENV.production ? ServiceWorkerModule.register('/ngsw-worker.js') : []
      ServiceWorkerModule.register('/ngsw-worker.js', { enabled: ENV.production })
    ],
  
    ...
  
  })
  export class CoreModule {
  
    ...
    
  }

Note: There seems to be a problem when registering a service worker in a module where AngularFire2 is also imported. I had to update my main.ts as follows:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

import { ENV } from '@env';

if (ENV.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule).then(() => {

  if ('serviceWorker' in navigator && ENV.production) {
    navigator.serviceWorker.register('/ngsw-worker.js');
    console.log('SW registered');
  }

}).catch(error => console.log(error));

ngsw-config.json

Now we're ready to create our service worker configuration file:

{
  "index": "/index.html",
  "assetGroups": [{
    "name": "Brew",
    "installMode": "prefetch",
    "resources": {
      "files": [
        "/index.html",
        "/manifest.json"
      ],
      "versionedFiles": [
        "/build/*.js",
        "/build/*.css"
      ],
      "urls": []
   }
  }, {
    "name": "assets",
    "installMode": "lazy",
    "updateMode": "prefetch",
    "resources": {
      "files": [
        "/assets/**"
      ],
      "urls": [
        "https://fonts.googleapis.com/**"
      ]
    }
  }]
}

package.json

We can update our npm scripts to include Angular Service Worker (NGSW) support:

"scripts": {

  ...
   
  "ngsw-config": "node_modules/.bin/ngsw-config www src/ngsw-config.json",
  "ngsw-copy": "cp node_modules/@angular/service-worker/ngsw-worker.js www/",
  "build": "ionic-app-scripts build && npm run ngsw-config && npm run ngsw-copy"
  
  ...
  
},

Now let's try a production build:

npm run build --prod

ngsw.json

And then take a look at NGSW's control file:

{
  "configVersion": 1,
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "Brew",
      "installMode": "prefetch",
      "updateMode": "prefetch",
      "urls": [
        "/index.html",
        "/manifest.json",
        "/build/0.js",
        "/build/1.js",
        "/build/10.js",
        "/build/2.js",
        
        ...
        
        "/build/main.css",
        "/build/main.js",
        "/build/polyfills.js",
        "/build/sw-toolbox.js",
        "/build/vendor.js"
      ],
      "patterns": []
    },
    {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "urls": [
        "/assets/fonts/noto-sans-bold.ttf"
        "/assets/fonts/noto-sans-bold.woff",
        "/assets/fonts/noto-sans-regular.ttf",
        "/assets/fonts/noto-sans-regular.woff",
        "/assets/fonts/noto-sans.scss",
        
        ...
        
        "/assets/icons/android-chrome-192x192.png",
        "/assets/icons/android-chrome-512x512.png",
        "/assets/icons/apple-touch-icon.png",
        "/assets/icons/favicon-16x16.png",
        "/assets/icons/favicon-32x32.png",
        "/assets/icons/favicon.ico",
        "/assets/icons/icon-1200x1200.png",
        "/assets/icons/icon-128x128.png",
        "/assets/icons/icon-144x144.png",
        "/assets/icons/icon-152x152.png",
        "/assets/icons/icon-192x192.png",
        "/assets/icons/icon-384x384.png",
        "/assets/icons/icon-512x512.png",
        "/assets/icons/icon-72x72.png",
        "/assets/icons/icon-96x96.png",
        "/assets/icons/mstile-150x150.png",
        "/assets/icons/safari-pinned-tab.svg"
      ],
      "patterns": []
    }
  ],
  "dataGroups": [],
  "hashTable": {
    "/build/0.js": "52a2e26203d073ee2e8704330b07f1cecf0d513e",
    "/build/1.js": "ed5cd1ba257c919835e853389ba43035aed20a4d",
    "/build/10.js": "71f52bb683be89d2f12caaf1d79b895d10d35a76",
    "/build/2.js": "e03b904da18dde1fd704d739b2c1ba2acb5879bc",
    
    ...
    
  }
}

Now we're ready to serve our app, for example:

firebase serve

And check (in Chrome DevTools) that our service worker has been registered:

What's Next

In the next post, I'll show you how to optimise your CSS delivery.

Resources: