Maxim Salnikov

@webmaxru

Workbox v4:

a brand new

workbox-window

module

Maxim Salnikov

  • PWA Slack organizer

  • PWA Oslo / PWA London meetups organizer

  • PWA speaker and trainer

  • Google Dev Expert in Web Technologies

Azure Developer Technical Lead at Microsoft

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

PWA news

#WSH?

"Progressive Web Apps State of the Union" by Dominick Ng at BlinkOn 10

"Progressive Web Apps State of the Union" by Dominick Ng at BlinkOn 10

BlinkOn

Microsoft & Samsung PWA Roundtable

Google I/O

Microsoft Build

April 8th

April 9-10th

May 6-8th

May 6-8th

  • 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

Application Shell

// 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

Injected by injectManifest() method of workbox-build module

Workbox manifest

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

Runtime caching

workbox.routing.registerRoute(
  new RegExp('/app/v2/'),
  workbox.strategies.networkFirst()
);

src/service-worker.js

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

Strategies

  • CacheFirst

  • CacheOnly

  • NetworkFirst

  • NetworkOnly

  • StaleWhileRevalidate

Plugins

  • Expiration

  • CacheableResponse

  • BroadcastUpdate

  • BackgroundSync

  • ...your own plugin?

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

Background sync

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

// 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 { Workbox } from 'workbox-window';

if ('serviceWorker' in navigator) {
    const wb = new Workbox('service-worker.js');

    // Event listeners...

    wb.register();
}

main.js

$ npm install workbox-window

Alternative

<script type="module">
import {Workbox} from 'https://...cdn.com/workbox-window.mjs';

if ('serviceWorker' in navigator) {
  const wb = new Workbox('service-worker.js');

  wb.register();
}
</script>

index.html

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.js

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

service-worker.js

Must have!

Other useful events

wb.addEventListener('activated', (event) => {
  // Service worker activated
});
wb.addEventListener('waiting', (event) => {
  // Service worker was installed but can't be activated
});

Same for the "external" service workers:

externalinstalled, externalwaiting, externalactivated

SW <-> Window communication

wb.addEventListener('message', (event) => {
  if (event.data.type === 'CACHE_UPDATE') {
    const {updatedURL} = event.data.payload;

    console.log(`A new version of ${updatedURL} is available!`);
  }
});

main.js

Alternative to "manual" BroadcastChannel API usage with workbox-broadcast-update package

Window <-> SW communication

wb.addEventListener('activated', (event) => {
  const urlsToCache = [...];
  // Send that list of URLs to your router in the service worker.
  wb.messageSW({
    type: 'CACHE_URLS',
    payload: {urlsToCache},
  });
});

main.js

Message format follows Flux standard action format 

Sample code

  • 1900+ developers

  • Major browsers/frameworks/libs reps

Thank you!

Maxim Salnikov

@webmaxru

Questions?

Maxim Salnikov

@webmaxru

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