with Angular + Workbox

https://slides.com/agesteira/pwas-with-angular-and-workbox

Andrés Gesteira

Twitter: Andres Gesteira (@GeorgeKaplan_G)

Loves 70s and 80s films.

Senior Product Engineer

Angular Developer

Lives in Berlin

Progressive

Web

Applications

What makes a Web App "progressive"?

It loads even when we are offline => App Shell

It is installable => Web App Manifest

It is cross-platform => Also desktop!

App Shell

It is the minimal HTML, CSS and JS to power the user interface.

You can think of it as the substitute of the SDK in a mobile context.

This approach relies on aggressively precaching files.

It shows the First Contentful Paint

The magic technology to do that is called Service Workers.

User Centric Metrics

From a user's perception page loads also have lifecycles 

Text

Screenshot from the Google I/O 2017 conference.

The App Manifest

A PWA is installable if the platform has support for this:

<link rel="manifest" href="/manifest.json">
{
  "short_name": "Maps",
  "name": "Google Maps",
  "icons": [
    {
      "src": "/images/icons-192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "/images/icons-512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "/?launch=pwa",
  "background_color": "#3367D6",
  "display": "standalone",
  "orientation": "landscape",
  "scope": "/maps/",
  "theme_color": "#3367D6"
}

Service Workers

JavaScript Threads

`window` in a browser.

`global` in Node.js

`self` in workers.

* If you just want to get the global object regardless of the context you need to use the `globalThis` property.

Workers

Web Workers: they offload heavy processing from the main thread.

*Worklets: they give access to low-level parts of the rendering pipeline.

Service Workers: event driven workers that act as a proxy servers.

* Houdini uses the `PaintWorklet` under the hood.

*Service Workers...

...are a replacement for the deprecated Application Cache.

...follow the the Extensible Web Manifesto philosophy.

...offer offline capabilities such as Push Notifications and Background Sync.

* Check out Jake Archibald's site about Service Workers.

...are request interceptors either to the Network or to the Cache Storage.

...only run over HTTPS or http://localhost, for security reasons.

Service Workers Lifecycle

Ensures that the page is controlled by only 1 version of the Service Worker.

It is made of 3 events:

  1. Download: the service worker is requested through a registration.
  2. Install: when the downloaded service worker file is found to be new.
  3. Activate: it allows the Service Worker to control clients (pages).

It can have 2 possible scenarios:

A. Newly created Service Worker.

B. Updated Service Worker.

Workbox

What is Workbox?

We register our Service Worker as normal...

...but in the worker thread we can start using the libraries:

It is a set of libraries that simplifies that process.

importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');

if (workbox) {
  console.log(`Yay! Workbox is loaded 🎉`);
} else {
  console.log(`Boo! Workbox didn't load 😬`);
}

Precaching

Wraps all 3 service worker lifecycle events with very few lines of code:

workbox.precaching.precacheAndRoute([
    '/styles/index.0c9a31.css',
    '/scripts/main.0d5770.js',
    { url: '/index.html', revision: '383676' },
]);

Runtime Caching

This is what we natively did with the fetch event but now using Workbox:

workbox.routing.registerRoute(
  '/logo.png',
  handler
);

Common Runtime Caching Strategies

The Workbox Strategies package handles the most common scenarios:

  • Cache Only: the Service Worker forces a response from the cache and never from the network.
  • Network Only: the Service Worker forces a response from the network and never from the cache.
  • Cache First falling back to network: the Service Worker tries the cache first and if there is no cached response it goes to the network. But most importantly: the response from the network is cached before being passed to the browser.
  • Network First falling back to cache: the Service Worker tries the network first. If the request is successful the response is cached before being passed to the browser. If the request fails it falls back to the last cached response.
  • Stale While Revalidate: here we only use responses from the cache but we also make a call to the network and if that call is successful we cache that response for the next time.

Check out the Rick And Morty PWA

A framework-agnostic workshop:

NGSW

Add a Service Worker

On an existing Angular project.

ng add @angular/pwa --project *project-name*

* Project flag is only necessary for non-default apps.

*

npm i lite-server -D
ng build --prod
npx lite-server --baseDir="dist/app-name"

Changes: package.json

@angular/pwa is an schematics package.

@angular/service-worker: ServiceWorkerModule, SwUpdate & SwPush.

lite-server is good for serving SPAs.

Changes: manifest.webmanifest (I)

The extension is irrelevant.

*.webmanifest is more convenient for some server configs.

Changes: manifest.webmanifest (II)

Changes: app.module.ts

Changes: ngsw-config.json (I)

Changes: ngsw-config.json (II)

dataGroups

export interface DataGroup {
  name: string;
  urls: string[];
  version?: number;
  cacheConfig: {
    maxSize: number;
    maxAge: string;
    timeout?: string;
    strategy?: 'freshness' | 'performance';
  };
}

freshness === Network First falling back to cache

performance === Cache First falling back to network

Build

Serve

Angular + Workbox (demo)

Initial settings

On an existing Angular project.

npm i lite-server lighthouse workbox-cli -D
"scripts": {
    "build": "ng build --prod",
    "lighthouse": "npx lighthouse http://localhost:3000/ --view --chrome-flags=\"--headless\"
                    --output-path=./lighthouse/lighthouse_\"$(date \"+%Y-%m-%d_%H-%M-%S\")\".report.html",
    "lint": "ng lint",
    "ng": "ng",
    "serve:dev": "ng serve",
    "serve:prod": "npx lite-server --baseDir=\"dist/app-name\"",
    "start": "npm run serve:dev"
  },
# .gitignore
/lighthouse/**.report.html
npm run build
npm run serve:prod
npm run lighthouse

Go offline => Dino Game

manifest.json

Generate Manifest with https://app-manifest.firebaseapp.com/

Copy icons folder to assets folder.

Copy generated file to src/manifest.json

Modify src/manifest.json.

Add src/manifest as an asset in angular.json.

index.html

Add meta tags:

Add link tags:

Add noscript tag:

<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Manifest name property" />
<meta name="description" content="Description" />
<meta name="theme-color" content="#xxx" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/assets/img/icons/angular-logo-512x512.png" />
<noscript>Please enable JavaScript to continue using this application.</noscript>
npm run build
npm run lighthouse

Add Service Worker (I)

Extend build script in package.json:

Create src/sw-custom.js:

"... && npx workbox copyLibraries dist/app-name && npx workbox injectManifest",
importScripts('/workbox-vx.x.x/workbox-sw.js');

if (workbox) {
  console.log(`Yay! Workbox is loaded 🎉`);

  workbox.setConfig({
    modulePathPrefix: '/workbox-vx.x.x/'
  });

  workbox.precaching.precacheAndRoute([]);

  workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL('/index.html'));
} else {
  console.log(`Boo! Workbox didn't load 😬`);
}

Add Service Worker (II)

Create workbox-config.js:

 

module.exports = {
  globDirectory: 'dist/app-name',
  globPatterns: ['**/*.{txt,ttf,otf,css,png,ico,html,js,json,mjs}'],
  swDest: 'dist/app-name/sw.js',
  swSrc: 'src/sw-custom.js',
  globIgnores: ['workbox-vx.x.x/**/*']
};

Add Service Worker (III)

Register Service Worker in main.ts

 

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .then(() => {
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js').then(
          registration => {
            console.log(`Service Worker registered! Scope: ${registration.scope}`);
          },
          error => {
            console.error(`Service Worker registration failed: ${error}`);
          }
        );
      });
    }
  })
  .catch(err => console.error(err));
npm run build
npm run lighthouse

Go offline => We keep our app

Install and uninstall

Warning

This implementation is going to change with Workbox 5.

NGSW vs Workbox

NGSW

Easy to start.

Seamless integration with Angular (SwUpdate, SwPush).

Includes Safety Worker.

Only allows 2 runtime caching strategies.

We cannot access the service worker.

Workbox

Full power of your own Service Worker.

Maximum flexibility with configurations.

Rich functionality for professional PWA development.

It requires a better understanding of Service Workers.

We lose @angular/service-worker features so we need to write them.

Wanna know more?

A series of articles coming soon

Workbox 5 implementation. 

How to replace SwUpdate & SwPush.

How to set caching routes and debug.

And much more.

Thank you!

Andres Gesteira @GeorgeKaplan_G

Made with Slides.com