NGSW or Workbox?

Maxim Salnikov

Angular GDE

Creating Angular PWA

How to create Angular Progressive Web App?

Using the proper tools

Maxim Salnikov

  • Google Developer Expert in Angular

  • Angular Oslo / PWA Oslo meetups organizer

  • ngVikings conference organizer

Products from the future

UI Engineer at ForgeRock

After all, what is PWA?

Progressive web apps use modern web APIs along with traditional progressive enhancement strategy to create cross-platform web applications.

These apps work everywhere and provide several features that give them the same user experience advantages as native apps.

#YearOfPWA

Latest updates

Cross-platform?

Browser

Desktop

Mobile

Flagged

UX advantages?

Smart networking + Offline

Proper app experience

Staying notified

Other cool things

}

Service Worker

API

Web App Manifest

Create Angular PWA

  • Code service worker manually

  • Use Angular Service Worker (NGSW)

  • Go for some PWA libraries

sw-precache

+

=

<script />

Service Worker 101

Wait a sec!

  • 1500+ developers

  • Major browsers/frameworks/libs reps

App shell

My App

Managing cache

self.addEventListener('install', (event) => {
  
    // Put app's html/js/css to cache

})
self.addEventListener('activate', (event) => {
  
    // Wipe previous version of app files from cache

})

In the real world

  • Can't add opaque responses directly

  • Redirected requests should be managed

  • Always creating a new version of cache and deleting the old one is not optimal

  • Control over cache size is required

  • Cache invalidation for runtime caching is complex

  • ...

Intercepting requests

self.addEventListener('fetch', (event) => {

  if (event.request.url.indexOf('/api') != -1) {
    event.respondWith(
      // Network-First Strategy
    )
  } else {
    event.respondWith(
      // Cache-First Strategy
    )
  }
})

In the real world

  • All kinds of fallbacks needed for the strategies

  • There are more complex strategies like Stale-While-Revalidate

  • Good to have routing

  • Good to have the possibility to provide some extra settings for different resource groups

  • ...

Pros

  • Great flexibility!

 

Cons

  • Great responsibility!

 

  • Implementing complex algorithms

  • Adopting best practices

  • Focusing on YOUR task

  • Following specifications updates

  • Handling edge cases

Tools help with

Angular Service Worker

NGSW

Generate a new Angular PWA

$ ng new myPWA --service-worker
  • Angular CLI 1.6-1.7 

  • Angular Service Worker 5

"Yesterday"

Generate a new Angular PWA

$ ng new myPWA
  • Angular CLI 6

  • Angular Service Worker 6

  • Web App Manifest

  • @Schematics

Today

$ ng add @angular/pwa

Building Angular PWA

$ ng build --prod

ngsw-worker.js

ngsw.json

dist/

safety-worker.js

assets/manifest.json

Checking the status

https://yourwebsite.com/ngsw/state

NGSW Debug Info:

Driver state: NORMAL ((nominal))
Latest manifest hash: cd4716ff2d3e24f4292010c929ff429d9eeead73
Last update check: 9s215u

=== Version 34c3fd2361735b1330a23c32880640febd059305 ===

Clients: 7eb10c76-d9ed-493a-be12-93f305394a77

=== Version cd4716ff2d3e24f4292010c929ff429d9eeead73 ===

Clients: ee22d69e-37f1-439d-acd3-4f1f366ec8e1

=== Idle Task Queue ===
Last update tick: 4s602u
Last update run: 9s222u
Task queue:


Debug log:

Registering NGSW

import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
...
@NgModule({
  ...
  imports: [
    ...


  ]
})
export class AppModule { }
    ServiceWorkerModule.register('/ngsw-worker.js',
        { enabled: environment.production }),

app.module.ts

NGSW configuration file

src/ngsw-config.json

{
  "index": "/index.html",
  "assetGroups": [...],
  "dataGroups": [...]
}

App shell

assetGroups

{
    "name": "app",
    "installMode": "prefetch",
    "resources": {...}
}

App shell resources

assetGroups / "app" / resources

"resources": {









}
    "versionedFiles": [
      "/*.bundle.css",
      "/*.bundle.js",
      "/*.chunk.js"
    ],
    "files": [
      "/favicon.ico",
      "/index.html"
    ],

App shell / on-demand

assetGroups

{
    "name": "assets",
    "installMode": "lazy",
    "updateMode": "prefetch",
    "resources": {...}
}

App shell / on-demand

assetGroups / "assets" / resources

"resources": {








}
    "files": [
      "/assets/**"
    ],
    "urls": [
        "https://fonts.googleapis.com/**",
        "https://fonts.gstatic.com/**"
    ]

Runtime caching

dataGroups

{
    "name": "api-freshness",
    "urls": [
      "/api/breakingnews/**"
    ],






}
    "cacheConfig": {
      "strategy": "freshness",
      "maxSize": 10,
      "maxAge": "12h",
      "timeout": "10s"
    }

Runtime caching

dataGroups

{
    "name": "api-performance",
    "urls": [
      "/api/archive/**"
    ],






}
    "cacheConfig": {
      "strategy": "performance",
      "maxSize": 100,
      "maxAge": "365d"
    }

Support API versioning

dataGroups

{
    "version": 1,
    "name": "api-performance",
    "urls": [
      "/api/**"
    ],
    ...
}
{
    "version": 2,
    "name": "api-performance",
    "urls": [
      "/api/**"
    ],
    ...
}

App version updates

v1

v2

v1

v1

v2

Server

Browser

v2

Notify about updates

import { SwUpdate } from '@angular/service-worker';
constructor(private swUpdate: SwUpdate) {}
this.swUpdate.available.subscribe(event => {
  let snackBarRef = this.snackBar
    .open('Newer version of the app is available', 'Refresh');

  snackBarRef.onAction().subscribe(() => {
    window.location.reload()
  })
})

updates.component.ts

Push notifications

import { SwPush } from '@angular/service-worker';
constructor(private swPush: SwPush) {}
subscribeToPush() {
  this.swPush.requestSubscription({
    serverPublicKey: this.VAPID_PUBLIC_KEY
  })
    .then(pushSubscription => {
      // Pass subscription object to backend
    })
}

push.component.ts

Push notifications / send

{
  "notification": {










  }
}

server-side.js / sendNotification payload

    "title": "Very important notification",
    "body": "Angular Service Worker is cool!",
    "icon": "https://angular.io/assets/logo.png",
    "actions": [
      {
        "action": "gocheck",
        "title": "Go and check"
      }
    ],
    ...

Kill switch

1. Long way

ng set apps.0.serviceWorker=false
ng build --prod
...deploy

2. Short way

rm dist/ngsw.json
...deploy

Kill switch

3. Proper way

cp dist/safety-worker.js dist/ngsw-worker.js
...deploy
self.addEventListener('install', e => { self.skipWaiting(); });

self.addEventListener('activate', e => {
  e.waitUntil(self.clients.claim());
  self.registration.unregister().then(
      () => { console.log('Unregistered old service worker'); });
});

safety-worker.js

Main available features

App Shell

Runtime Caching

Push Notifications

Smart Updates

Pros

  • Smart defaults go out of the box

  • Essential features are codeless

  • Doing things in Angular way

Cons

  • Intended to play a main service worker's role in your PWA

  • There is no good way to extend the functionality

 

+

=

# Install the Workbox CLI
$ npm install workbox-cli --global
# Run the wizard to generate a service worker
$ workbox wizard

App shell

Runtime caching

Offline GA

Replay failed requests

Broadcast updates

Build integrations

Workbox 101

module.exports = {
  "globDirectory": "dist/",
  "globPatterns": [
    "**/*.{txt,png,ico,html,js,json,css}"
  ],
  "swDest": "dist/sw.js"
};

workbox-config.js

App shell

module.exports = {
  "globDirectory": "dist/ngPwa",
  "globPatterns": [
    "favicon.ico",
    "index.html",
    "*.css",
    "*.js",
    "assets/**/*"
  ],
  "swDest": "dist/ngPwa/sw.js"
};

workbox-config.js

App shell

importScripts("https://storage.googleapis.com/.../workbox-sw.js");

self.__precacheManifest = [
 {
    "url": "index.html",
    "revision": "b3429974cda6f87f84df90b35fc6faa4"
 }, ...
].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

sw.js

Generated service worker

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

    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('sw-default.js')
    }

  });

main.ts

Registration

Caching and serving

Our own service worker

"assets": [
    {
      "glob": "**",
      "input": "../libraries/",
      "output": "./"
    }
  ...
]

.angular-cli.json

$ npm install --save-dev workbox-build
$ workbox copyLibraries libraries/

Our own service worker

const {injectManifest} = require('workbox-build')
const swSrc = 'src/service-worker.js';
const swDest = 'dist/sw-default.js'

injectManifest({
  swSrc, swDest, ...
  })
  .then(({count, size}) => {
    console.log(`Generated SW which will
      precache ${count} files,
      totaling ${size} bytes.`)
  })

workbox-build.js

Our own service worker

importScripts("workbox-v3.2.0/workbox-sw.js");

workbox.precaching.precacheAndRoute([])

my-serviceworker.js

// Features code

workbox.routing.registerRoute(
  /(http[s]?:\/\/)?([^\/\s]+\/)timeline/,
  workbox.strategies.networkFirst()
)
...

Pros

  • Can extend existing service worker

  • Feature-rich

 

Cons

  • Extra build step needed

Thank you!

Questions?

Made with Slides.com