loading...
Cover image for PWAs with Angular: Being Reliable

PWAs with Angular: Being Reliable

michaelsolati profile image Michael Solati Updated on ・5 min read

If you've been in the world of web development during the past two years you've probably heard the term Progressive Web Apps (PWAs for short). PWAs are essentially web applications that provide a near-native experience on mobile devices. According to Google they must be:

  • Reliable - Load instantly and never show the downasaur, even in uncertain network conditions.
  • Fast - Respond quickly to user interactions with silky smooth animations and no janky scrolling.
  • Engaging - Feel like a natural app on the device, with an immersive user experience.

So how can we make sure our Angular applications follow these tenets and provide the best user experiences?

Pages loading offline? Now that's reliable!


Well let's take a simple app I've already written and turn it into a PWA.

git clone --branch v0.0 https://github.com/MichaelSolati/ng-popular-movies-pwa.git
cd ng-popular-movies-pwa
npm install

This app depends on The MovieDB's APIs. Get an API key (check this out) and put it as the moviedb environment variable in your src/environments/environment.ts and src/environments/environment.prod.ts.

Now that we have everything set up, lets run our application npm run start:pwa, open up Chrome and go to localhost:8080 and see what it does.

Built app running

Well it runs, and if you click on a movie we can get more details about it. AWESOME! But what happens when we're offline?

Built app offline

Hmm… that’s not that good. If there was a test for PWAs that would definitely fail it…

Wait a sec! There is a test for PWAs! Google provides an extension for Chrome called Lighthouse (install it!) that will run a barrage of tests against our application and will generate a report on how well the app did. And, as I expected, this app failed hard.

Not that good of a PWA


We can do better than this, so lets make this app more reliable!

The first thing we can do to address a lot of these issues is to use a Service Worker. A Service Worker enables our PWA to load instantly, regardless of the network state. It sits as a proxy between our application and the outside world, and puts us in control of the cache and how to respond to resource requests. Our Service Worker allows us to cache essential resources to cut down on our dependence on the network, giving an instant and reliable experience for your users even when there is no internet.

Now the Angular team actually provides us with an easy tool to add a Service Worker as well as another set of tool (we'll get into them later), so let's install our tools with the following.

npm install --save @angular/service-worker @angular/platform-server ng-pwa-tools

That was easy, but we will also need to update our .angular-cli.json to tell it we want to include a Service Worker. That way, every time we make a production build, it will include our Service Worker.

ng set apps.0.serviceWorker=true

Now if we run a production build ng build --prod and check our dist/ folder, we'll see a file called ngsw-manifest.json and if we look into it we will see all of our assets that will be cached by our Service Worker.

{
  "static": {
    "urls": {
      "/polyfills.859f19db95d9582e19d4.bundle.js": "afac7bb7a75d8e31bca1d0a21bc8a8b8d5c8043c",
      "/main.9058c5e7c9cdfe8d2b7e.bundle.js": "93293a45586e8923695e614746ae61d658cde5ed",
      "/sw-register.e4d0fe23aa9c2f3a68bb.bundle.js": "2a8aea5c32b446b61dab2d7c18231c4527f04bdc",
      "/vendor.1fd4688f90e61a7dc14d.bundle.js": "92513639a29f19b868733d40bb37732fc051b326",
      "/inline.6e6ae94836243f3c1fa2.bundle.js": "7e89339e980b3fe1ac59ed6ee44800ad1c647084",
      "/styles.b11de945749bdbf0b1ca.bundle.css": "3e920bb539d1da98370748436c09677e81a50d46",
      "/assets/.DS_Store": "edc93fc6e9f594928b74bd2e15a23417aa68ac5d",
      "/assets/app-icon.png": "cd65256eb15ba9d4150e783ddaf93399799f605f",
      "/favicon.ico": "c31b53fba70406741520464040435aabaaed370e",
      "/index.html": "3953d6c604ff7dc6b9e77e8310cd7877d2b49b0d"
    },
    "_generatedFromWebpack": true
  }
}

But this doesn't include our routing configuration, so we can create one with the ngu-sw-manifest tool (part of ng-pwa-tools we installed before).

./node_modules/.bin/ngu-sw-manifest --module src/app/app.module.ts

Now, you probably got an error like this…

ENOENT: no such file or directory, open 'app.component.html' ;

Our ngu-sw-manifest tool isn't able to traverse through our application when there are relative paths for our component's templates and stylesheets. So we'll add moduleId: module.id to the @Component decorator of our three components. (src/app/app.component.ts, src/app/home/home.component.ts, and src/app/movie/movie.component.ts) So we should have decorators that look like this.

@Component({
  moduleId: module.id,
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})

And if we run the failed ngu-sw-manifest command again we should see:

{
  "routing": {
    "index": "/index.html",
    "routes": {
      "/": {
        "match": "exact"
      },
      "^/movie/[^/]+$": {
        "match": "regex"
      },
      "/movie": {
        "match": "exact"
      },
      "^/.*$": {
        "match": "regex"
      }
    }
  },
  "static": {
    "urls": {
      "/favicon.ico": "c31b53fba70406741520464040435aabaaed370e",
      "/index.html": "551a50f7e2847f7ed85cda1f8e4b7877bfdbb492",
      "/inline.341ede62d3a808c130e1.bundle.js": "79d61daf91c3d745aac6c274fadc4ac826332358",
      "/main.07650488997a7b2dfcc1.bundle.js": "49be3a9f04ebc4383806652e13f3be4ca58b3902",
      "/ngsw-manifest.json": "29a96adf2f918b27cc37be64b7ee24d15d095963",
      "/polyfills.859f19db95d9582e19d4.bundle.js": "afac7bb7a75d8e31bca1d0a21bc8a8b8d5c8043c",
      "/styles.b11de945749bdbf0b1ca.bundle.css": "3e920bb539d1da98370748436c09677e81a50d46",
      "/sw-register.e4d0fe23aa9c2f3a68bb.bundle.js": "2a8aea5c32b446b61dab2d7c18231c4527f04bdc",
      "/vendor.f47d925e37c84559515b.bundle.js": "cd70a6deaa413652cc98b444f793f5cf1e837be6",
      "/worker-basic.min.js": "93904d94c0bef0479f1ec0b182788f4301d9f28e",
      "/assets/.DS_Store": "edc93fc6e9f594928b74bd2e15a23417aa68ac5d",
      "/assets/app-icon.png": "cd65256eb15ba9d4150e783ddaf93399799f605f"
    }
  }
}

We're going to work with the above object in a bit, but first in our src/ directory we're going to create a ngsw-manifest.json file and fill it like so.

{
  "dynamic": {
    "group": [
      {
        "name": "firebase",
        "urls": {
          "https://ng-popular-movies-pwa.firebaseapp.com/": { // Our deployed app url
            "match": "prefix"
          }
        },
        "cache": {
          "optimizeFor": "performance", // grabs data from cache only if data is stale
          "maxAgeMs": 3600000, // cache for about an hour
          "maxEntries": 20, // minimize cache size
          "strategy": "lru" // tells service worker how to remove cached date (least recently used first)
        }
      }
    ]
  }
}

This sets our default caching strategy. (Modify the dynamic.groups[0].name and dynamic.groups[0].urls based on how you plan on hosting your app) Just remove the comments included, and now we can run the ngu-sw-manifest tool to take our assets, routing, and our custom defined manifest file and output it into our dist/ngsw-manifest.json.

./node_modules/.bin/ngu-sw-manifest --module src/app/app.module.ts \
   --out dist/ngsw-manifest.json

So now we're quickly going to update our npm scripts to the following:

{
    "ng": "ng",
    "start": "ng serve",
    "start:pwa": "npm run build && cd dist && http-server",
    "build": "ng build --prod && npm run ngu-sw-manifest",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "ngu-sw-manifest": "./node_modules/.bin/ngu-sw-manifest --module src/app/app.module.ts --out dist/ngsw-manifest.json"
  }

Then we can run our app npm run start:pwa. Try running the application after disabling the network connection in the Chrome Dev Console, heck we can even run Lighthouse on it again!

Working offline!

And a slightly better PWA score =)

So we're doing a little bit better (some things like the HTTPS we wouldn't be able to fix until we deployed this). But at least we've doubled that PWA score! Next week we're going to up that score by improving our speed (perceived and real).


To look at the code differences from where we started to right now click here. Also if you feel so inclined, I invite you to deploy up your app to Firebase. If you run Lighthouse on the deployed app you'll get a much higher score:

Lighthhouse on deployed app


Part two, titled "PWAs with Angular: Being Fast," is available here.

Posted on by:

michaelsolati profile

Michael Solati

@michaelsolati

Developer 🥑 for Google with a love for Java (think Android), Java (think Starbucks), and JavaScript.

Discussion

pic
Editor guide
 

My experience running this tutorial:

I had to install some missing dependencies:
npm install -g reflect-metadata
npm install -s @angular/cdk

I also upgraded the cli:
npm install -s @angular/cli@latest

I checked for updates:
npm-check-updates -u

To run the ngu-sw-manifest I had to use the npm-run utility:
npm-run ngu-sw-manifest --module src/app/app.module.ts

I finally hit a road block:
$ npm-run ngu-sw-manifest --module src/app/app.module.ts --out dist/ngsw-manifest.json

/home/stephane/dev/js/projects/angular/ng-popular-movies-pwa/node_modules/ng-pwa-tools/lib/sw-manifest/index.js:34
manifest = JSON.parse(fs.readFileSync(args.manifest).toString());
^
SyntaxError: Unexpected token / in JSON at position 148
at JSON.parse ()
at Object.generateSwManifest (/home/stephane/dev/js/projects/angular/ng-popular-movies-pwa/node_modules/ng-pwa-tools/lib/sw-manifest/index.js:34:25)
at Object. (/home/stephane/dev/js/projects/angular/ng-popular-movies-pwa/node_modules/ng-pwa-tools/bin/ngu-sw-manifest.js:18:15)
at Module._compile (module.js:569:30)
at Object.Module._extensions..js (module.js:580:10)
at Module.load (module.js:503:32)
at tryModuleLoad (module.js:466:12)
at Function.Module._load (module.js:458:3)
at Function.Module.runMain (module.js:605:10)
at Object. (/home/stephane/dev/js/projects/angular/ng-popular-movies-pwa/node_modules/ts-node/src/_bin.ts:177:12)

 

There have been some changes to the @angular/material where @angular/cdk has been broken out and needs to be installed separately as you said. Also there is an issue with lazy loading with the latest CLI building which requires the installation of enhanced-resolve you can see here.

In regards to the service worker stuff I would need to see your code before I can say anything for sure.

 

I just updated the tags and codebase on GitHub, it should be all set to go.

 

Thank you for the update. I git cloned it again then, but ended up in the ditch again.

$ npm-run ngu-sw-manifest --module src/app/app.module.ts
/usr/bin/env: «node_modules/.bin/ts-node»: Aucun fichier ou dossier de ce type

Sorry for being an PWA noob.

Are you on Windows? Also the following command should work... ./node_modules/.bin/ngu-sw-manifest --module src/app/app.module.ts

I'm on Linux, a 30 year man. I first coded on our Atari 800XL in 1984. I did use Windows at times though, on some contract jobs.

I get the same response with your command:
$ ./node_modules/.bin/ngu-sw-manifest --module src/app/app.module.ts
/usr/bin/env: «node_modules/.bin/ts-node»: Aucun fichier ou dossier de ce type
[stephane@stephane-ThinkPad-X201 ng-popular-movies-pwa ((v0.0))]
$ npm-run ngu-sw-manifest --module src/app/app.module.ts
/usr/bin/env: «node_modules/.bin/ts-node»: Aucun fichier ou dossier de ce type

I feel like this issue of mine is not related to the actual content of your tutorial, and thus don't want to bother you with it. I shall post on SO if that is of interest to the Javascript Angular crowd.

Haha, ok, well I have one more idea... node ./node_modules/.bin/ngu-sw-manifest --module src/app/app.module.ts

I posted on SO so as not to hijack your comments thread here.

stackoverflow.com/questions/455714...