Maxim Salnikov

Angular GDE

Sending the Angular app into deep, deep offline with Workbox

How to build offline-ready Angular app

Using the framework-agnostic library

Maxim Salnikov

  • Google Developer Expert in Angular

  • Angular Oslo / PWA Oslo meetups organizer

  • ngVikings /  ngCommunity organizer

Products from the future

UI Engineer at ForgeRock

What is PWA at all?

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.

works everywhere*

* but not everything**

natively

** use progressive enhancement strategy

Create Angular PWA

  • Code service worker manually

  • Use Angular Service Worker (NGSW)

  • Use some PWA libraries

Minimum viable PWA

=

+

Application shell

Web App Manifest

Fast, responsive, mobile-first

Served via HTTPS

Let's build an App shell

My App

  • Pick only the files we need

  • Create the list of files and their hashes

  • First load: put these files into the Cache Storage

  • Next loads: serve them from Cache Storage

  • If some files were updated (hashes comparison) put their new versions into the Cache Storage and remove old ones *

  • On the n+1 load - serve the updated  files

The app was updated.

Refresh?

1

2

3

$ ng add @angular/pwa

Scaffold

$ ng build --prod

Build

Run

ngsw-worker.js

Smart configuration

Service worker manifest

Taking care of all the rest

Angular Service Worker

Angular Service Worker

  • Easy to start

  • Seamless integration with Angular

  • Coding-free basic features

  • Angular-friendly approach

Add -> Configure

Get what's included:

Application shell, Runtime caching, Push notifications

  • Application shell

  • Runtime caching

  • Replaying failed network requests

  • Offline Google Analytics

  • Broadcasting updates

  • Registration helper

Have our own service worker!

Working modes

  • Workbox CLI

  • Webpack plugin

  • Node module

# Installing the Workbox Node module
$ npm install workbox-build --save-dev

Also installs all Workbox libraries via dependencies

Build script

// We will use injectManifest mode
const {injectManifest} = require('workbox-build')

// Sample configuration with the basic options
var workboxConfig = {...}

// Calling the method and output the result
injectManifest(workboxConfig).then(({count, size}) => {
    console.log(`Generated ${workboxConfig.swDest},
    which will precache ${count} files, ${size} bytes.`)
})

workbox-build-inject.js

Build script configuration

// Sample configuration with the basic options
var workboxConfig = {
  globDirectory: 'dist/angular-pwa/',
  globPatterns: [
    '**/*.{txt,png,ico,html,js,json,css}'
  ],
  swSrc: 'src/service-worker.js',
  swDest: 'dist/angular-pwa/service-worker.js'
}

workbox-build-inject.js

Source service worker

// Importing Workbox itself from Google CDN
importScripts('https://googleapis.com/.../workbox-sw.js');

// Precaching and setting up the routing
workbox.precaching.precacheAndRoute([])

src/service-worker.js

1

2

Precaching manifest

[
  {
    "url": "index.html",
    "revision": "34c45cdf166d266929f6b532a8e3869e"
  },
  {
    "url": "favicon.ico",
    "revision": "b9aa7c338693424aae99599bec875b5f"
  },
  ...
]

Build flow integration

{
  "scripts": {
    "build-prod": "ng build --prod &&
                   node workbox-build-inject.js"
  }
}

package.json

Better app update UX

App version updates

v1

v2

v1

v1

v2

Deployed

Displayed

v2

A new version of the app is available. Click to refresh.

const updateChannel = new BroadcastChannel('app-shell');

updateChannel.addEventListener('message', event => {
    // Inform about the new version & prompt to reload
});

Option #1: BroadcastChannel

updates.component.ts

workbox.precaching.addPlugins([
    new workbox.broadcastUpdate.Plugin('app-shell')
]);

src/service-worker.js

3

// Feature detection
if ('serviceWorker' in navigator) {

  // Postponing the registration for better performance
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js');
  });

}

Option #2: Service worker lifecycle

app.js

Workbox v4: workbox-window

import { register } from 'register-service-worker'

platformBrowserDynamic().bootstrapModule(AppModule)
  .then( () => {
    if ('serviceWorker' in navigator) {
      const wb = new Workbox('service-worker.js');
      // Event listeners...
      wb.register();
    }
  })

main.ts

$ npm install workbox-window

Was service worker file updated?

wb.addEventListener('installed', event => {
  if (event.isUpdate) {
  // Show "Newer version is available. Refresh?" prompt
  } else {
  // Optionally: show "The app is offline-ready" toast
  }
});

main.ts

3

workbox.core.skipWaiting()
workbox.core.clientsClaim()

service-worker.js

Must have!

Runtime caching

Routes and strategies

workbox.routing.registerRoute(
  new RegExp('/api/'),
  new workbox.strategies.NetworkFirst()
);

src/service-worker.js

workbox.routing.registerRoute(
  new RegExp('/images/'),
  new workbox.strategies.CacheFirst({
    plugins: [...]
  })
);

Strategies

  • CacheFirst

  • CacheOnly

  • NetworkFirst

  • NetworkOnly

  • StaleWhileRevalidate

Plugins

  • Expiration

  • CacheableResponse

  • BroadcastUpdate

  • BackgroundSync

  • ...your own plugin?

Background syncronization

const postTweetPlugin =
    new workbox.backgroundSync.Plugin('tweetsQueue', {
        maxRetentionTime: 24 * 60 // Max retry period
    })

src/service-worker.js

workbox.routing.registerRoute(
  /(http[s]?:\/\/)?([^\/\s]+\/)post-tweet/,
  new workbox.strategies.NetworkOnly({
    plugins: [postTweetPlugin]
  }),
  'POST'
)

Bundling

Deploying only what we use

import { precacheAndRoute }
  from 'workbox-precaching/precacheAndRoute.mjs'
import { skipWaiting }
  from 'workbox-core/skipWaiting.mjs'
import { clientsClaim }
  from 'workbox-core/clientsClaim.mjs'

skipWaiting()
clientsClaim()

precacheAndRoute([])

src/service-worker-bundle.js

Using bundler

import resolve from 'rollup-plugin-node-resolve'
import replace from 'rollup-plugin-replace'
import { terser } from 'rollup-plugin-terser'

export default {
  input: 'dist/angular-pwa/service-worker.js',
  output: {
    file: 'dist/angular-pwa/service-worker.js',
    format: 'iife'
  },
  plugins: [...]
}

rollup.config.js

Needed plugins

plugins: [
  resolve(),
  replace({
    'process.env.NODE_ENV': JSON.stringify('production')
  }),
  terser()
]

rollup.config.js / plugins

Build script

"build-pwa-bundle":
  "ng build --prod &&
   node workbox-build-inject.js &&
   npx rollup -c"

package.json / scripts

Resulting service-worker.js:

Summary

  • Framework-agnostic

  • Rich functionality

  • Maximum flexible configuration

  • Full power of our own service worker

  • Ready for transpilation, tree-shaking, bundling

Setup -> Configure -> Code

Get what you want

Sample code

  • 1900+ developers

  • Major browsers/frameworks/libs reps

Thank you!

Maxim Salnikov

@webmaxru

Questions?

Maxim Salnikov

@webmaxru

Sending the Angular app into deep, deep offline with Workbox

By Maxim Salnikov

Sending the Angular app into deep, deep offline with Workbox

There is no need to advocate for progressive web apps anymore. The idea of connection-independent applications has proven its viability and we see more and more large and small projects following that path, making the offline-ready behavior a best practice, good manner of the web. In my session, based on the deep exploration of Service Worker API possibilities and gathered UX gotchas, we go through the history of the offline web, the importance of treating the network as an enhancement, current challenges (and their solutions) and proper tooling. We architect our offline-ready Angular app applying the best tech and UX practices adding the features one-by-one: app shell, caching resources and data, sync when online. All in name of our users who demand the new level of the resilient web experience.

  • 1,515