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

#WSH?

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.

Create Angular PWA

  • Code service worker manually

  • Use Angular Service Worker (NGSW)

  • Use some PWA libraries

sw-precache

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

Service Worker 101

Logically

Physically

-file(s)

Website

Service-worker

Browser/OS

Event-driven worker

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

  • Application shell

  • Runtime caching

  • Push notifications

Automation

Scaffolding

Building

Running

Schematics

Angular CLI

NGSW

$ ng add @angular/pwa

Scaffold

$ ng build --prod

Build

Run

ngsw-worker.js

Smart configuration

Service worker manifest

Taking care of all the rest

NGSW

  • Easy to start

  • Seamless integration with Angular

  • Coding-free basic features

  • Angular-friendly approach

Add -> Configure

Get what's included

  • Application shell

  • Runtime caching

  • Replaying failed network requests

  • Offline Google Analytics

  • Broadcasting updates

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

Precaching manifest

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

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

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

Requirements

  • Feature detection

  • Registration after app fully loaded and UI rendered

  • Hook into service worker lifecycle update event

  • Was the service worker updated?

  • Was the app itself updated?

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'
)

Push notifications

Out of Workbox scope

Notifications handling

self.addEventListener('push', (event) => {
    self.registration.showNotification(...)
})

src/service-worker.js

self.addEventListener('notificationclick', (event) => {
    // React on notification actions
})
self.addEventListener('notificationclose', (event) => {
    // React on notification closing
})

Local version

Copy libraries

const {copyWorkboxLibraries} = require('workbox-build')

copyWorkboxLibraries('libraries')
  .then((dir) => {
    console.log(`Success copying libraries to ${dir}`)
  })

workbox-copy-libraries.js

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

angular.json / projects / {name} / architect / build / options

Use local versions via workbox-sw

importScripts('workbox-v4.0.0/workbox-sw.js')

workbox.setConfig({modulePathPrefix: 'workbox-v4.0.0/'})

src/service-worker.js

  • Creates workbox object

  • Only loads the packages we use

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

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,681