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