Maxim Salnikov
Angular GDE
How to build offline-ready Angular app
Products from the future
UI Engineer at ForgeRock
Progressive web apps use modern web APIs along with traditional progressive enhancement strategy to create cross-platform web applications.
My App
The app was updated.
Refresh?
Website
Service-worker
Browser/OS
Event-driven worker
self.addEventListener('install', (event) => {
// Put app's html/js/css to cache
})
self.addEventListener('activate', (event) => {
// Wipe previous version of app files from cache
})
self.addEventListener('fetch', (event) => {
if (event.request.url.indexOf('/api') != -1) {
event.respondWith(
// Network-First Strategy
)
} else {
event.respondWith(
// Cache-First Strategy
)
}
})
$ ng add @angular/pwa
$ ng build --prod
# Installing the Workbox Node module
$ npm install workbox-build --save-dev
// 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.`)
})
// 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'
}
[
{
"url": "index.html",
"revision": "34c45cdf166d266929f6b532a8e3869e"
},
{
"url": "favicon.ico",
"revision": "b9aa7c338693424aae99599bec875b5f"
},
...
]
// Importing Workbox itself from Google CDN
importScripts('https://googleapis.com/.../workbox-sw.js');
// Precaching and setting up the routing
workbox.precaching.precacheAndRoute([])
{
"scripts": {
"build-prod": "ng build --prod &&
node workbox-build-inject.js"
}
}
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
});
workbox.precaching.addPlugins([
new workbox.broadcastUpdate.Plugin('app-shell')
]);
// Feature detection
if ('serviceWorker' in navigator) {
// Postponing the registration for better performance
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js');
});
}
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();
}
})
$ npm install workbox-window
wb.addEventListener('installed', event => {
if (event.isUpdate) {
// Show "Newer version is available. Refresh?" prompt
} else {
// Optionally: show "The app is offline-ready" toast
}
});
workbox.core.skipWaiting()
workbox.core.clientsClaim()
workbox.routing.registerRoute(
new RegExp('/api/'),
new workbox.strategies.NetworkFirst()
);
workbox.routing.registerRoute(
new RegExp('/images/'),
new workbox.strategies.CacheFirst({
plugins: [...]
})
);
const postTweetPlugin =
new workbox.backgroundSync.Plugin('tweetsQueue', {
maxRetentionTime: 24 * 60 // Max retry period
})
workbox.routing.registerRoute(
/(http[s]?:\/\/)?([^\/\s]+\/)post-tweet/,
new workbox.strategies.NetworkOnly({
plugins: [postTweetPlugin]
}),
'POST'
)
Out of Workbox scope
self.addEventListener('push', (event) => {
self.registration.showNotification(...)
})
self.addEventListener('notificationclick', (event) => {
// React on notification actions
})
self.addEventListener('notificationclose', (event) => {
// React on notification closing
})
const {copyWorkboxLibraries} = require('workbox-build')
copyWorkboxLibraries('libraries')
.then((dir) => {
console.log(`Success copying libraries to ${dir}`)
})
"assets": [{
"glob": "**", "input": "./libraries/", "output": "./"
}]
importScripts('workbox-v4.0.0/workbox-sw.js')
workbox.setConfig({modulePathPrefix: 'workbox-v4.0.0/'})
import { precacheAndRoute }
from 'workbox-precaching/precacheAndRoute.mjs'
import { skipWaiting }
from 'workbox-core/skipWaiting.mjs'
import { clientsClaim }
from 'workbox-core/clientsClaim.mjs'
skipWaiting()
clientsClaim()
precacheAndRoute([])
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: [...]
}
plugins: [
resolve(),
replace({
'process.env.NODE_ENV': JSON.stringify('production')
}),
terser()
]
"build-pwa-bundle":
"ng build --prod &&
node workbox-build-inject.js &&
npx rollup -c"
Maxim Salnikov
@webmaxru
Maxim Salnikov
@webmaxru