Maxim Salnikov
Angular GDE
Sending the Angular app into deep, deep offline with Workbox
How to build an offline-ready Angular app
Using a framework-agnostic library
Maxim Salnikov
-
Angular Oslo, PWA Oslo meetups organizer
-
ngVikings / Mobile Era conferences organizer
-
Google Dev Expert in Angular
Developer Engagement Lead at Microsoft
Web as an app platform
-
Historically depends on the "connection status"
-
Evergreen browsers
-
Performant JS engines
-
Excellent tooling
-
Huge community
Proper offline-ready web app
-
App itself
-
Online runtime data
-
Offline runtime data
-
Connection failures
-
Updates
-
Platform features
-
Always available
-
Thoughtfully collected
-
Safely preserved
-
Do not break the flow
-
Both explicit and implicit
-
For the win!
While keeping its web nature!
Precaching the app [shell] itself
-
Define assets
-
Put in the cache
-
Serve from the cache
-
Manage versions
}
Service worker
Hereafter: "cache" = Cache Storage
Service worker 101
App
Service worker
Cache
fetch
self.addEventListener('fetch', event => {
// Serve assets from cache or network
})
handmade-service-worker.js
Browser
self.addEventListener('install', event => {
// Use Cache API to cache html/js/css
})
self.addEventListener('activate', event => {
// Manage versions
})
What could go wrong?
Redirects?
Fallbacks?
Opaque response?
Versioning?
Cache invalidation?
Spec updates?
Cache storage space?
Variable asset names?
Feature detection?
Minimal required cache update?
Caching strategies?
Routing?
Fine-grained settings?
Kill switch?
I see the old version!!!
-
Define assets
-
Put in the cache
-
Serve from the cache
-
Manage versions
}
Options for Angular
Angular
Service Worker
-
Precaching and routing
-
Runtime caching strategies
-
Replaying failed network requests
-
Recipes for quick start
-
Service worker communication helpers
+ full control over the service worker
Setting up
-
Create a source service worker
-
Inject app assets versioned list
-
Bundle and compress
-
Register SW in the app
# Installing the Workbox Node module
$ npm install workbox-build --save-dev
}
On every app build
Source service worker
import {
precacheAndRoute,
createHandlerBoundToURL
} from "workbox-precaching";
import { NavigationRoute, registerRoute } from "workbox-routing";
// Precaches and routes resources from __WB_MANIFEST array
precacheAndRoute(self.__WB_MANIFEST);
// Setting up navigation for SPA
const navHandler = createHandlerBoundToURL("/index.html");
const navigationRoute = new NavigationRoute(navHandler);
registerRoute(navigationRoute);
src/service-worker.js
Injecting app asset list
const { injectManifest } = require("workbox-build");
let workboxConfig = {
swSrc: "src/service-worker.js",
swDest: "dist/prog-web-news/sw.js",
// + more on the next slide
};
injectManifest(workboxConfig).then(() => {
console.log(`Generated ${workboxConfig.swDest}`);
});
workbox-inject.js
Config for Angular
globDirectory: "dist/prog-web-news",
globPatterns: ["index.html", "*.css", "*.js", "assets/**/*"],
globIgnores: [
"**/*-es5.*.js", // Skip ES5 bundles for Angular
],
// Angular takes care of cache busting for JS and CSS (in prod mode)
dontCacheBustURLsMatching: new RegExp(".+.[a-f0-9]{20}.(?:js|css)"),
// By default, Workbox will not cache files larger than 2Mb
// (might be an issue for dev builds)
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, // 4Mb
workbox-inject.js / workboxConfig object
[Almost] ready service worker
import { precacheAndRoute } from "workbox-precaching";
import { NavigationRoute, registerRoute } from "workbox-routing";
...
precacheAndRoute([
{ revision: "866bcc582589b8920dbc5bccb73933b1", url: "index.html" },
{ revision: null, url: "styles.c2761edff7776e1e48a3.css" },
{ revision: null, url: "main.3469613435532733abd9.js" },
{ revision: null, url: "polyfills.25b2e0ae5a439ecc1193.js" },
{ revision: null, url: "runtime.359d5ee4682f20e936e9.js" },
{
revision: "33c3a22c05e810d2bb622d7edb27908a",
url: "assets/img/pwa-logo.png",
},
]);
dist/prog-web-news/sw.js
Bundling and compressing
import resolve from 'rollup-plugin-node-resolve'
import replace from 'rollup-plugin-replace'
import { terser } from 'rollup-plugin-terser'
export default {
input: 'dist/prog-web-news/sw.js',
output: {
file: 'dist/prog-web-news/sw.js',
format: 'iife'
},
plugins: [ /* Next slide */ ]
}
rollup.config.js
Rollup plugins configuration
plugins: [
resolve(),
replace({
'process.env.NODE_ENV': JSON.stringify('production')
}),
terser()
]
rollup.config.js / plugins
Gathering all together
"build-pwa":
"ng build --prod &&
node workbox-inject.js &&
npx rollup -c"
package.json / scripts
Resulting dist/prog-web-news/sw.js on every build
Service worker registration
import { Workbox, messageSW } from 'workbox-window';
...
ngOnInit(): void {
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js');
wb.register();
// Reload-to-Update flow Using messageSW
// See demo repo aka.ms/angular-workbox
}
}
app-shell.component.ts
Demo
Runtime caching
import { registerRoute } from "workbox-routing";
import {
CacheFirst,
NetworkFirst,
StaleWhileRevalidate,
} from "workbox-strategies";
src/service-worker.js
// Gravatars can live in cache
registerRoute(
new RegExp("https://www.gravatar.com/avatar/.*"),
new CacheFirst()
);
Runtime caching for API
// Keeping lists always fresh
registerRoute(
new RegExp("https://progwebnews-app.azurewebsites.net.*posts.*"),
new NetworkFirst()
);
// Load details immediately and check and inform about update right after
import { BroadcastUpdatePlugin } from 'workbox-broadcast-update';
registerRoute(
new RegExp("https://progwebnews-app.azurewebsites.net.*posts/slug.*"),
new StaleWhileRevalidate({
plugins: [
new BroadcastUpdatePlugin(),
],
})
);
src/service-worker.js
Strategies
-
CacheFirst
-
CacheOnly
-
NetworkFirst
-
NetworkOnly
-
StaleWhileRevalidate
-
...your custom strategy?
Plugins
-
Expiration
-
CacheableResponse
-
BroadcastUpdate
-
BackgroundSync
-
...your own plugin?
SW lifecycle hack
import { clientsClaim } from "workbox-core";
// Claiming control to start runtime caching asap
clientsClaim();
src/service-worker.js
Fetching data
Registering SW
Application's first load timeline
Workbox recipes
import {
googleFontsCache,
imageCache
} from "workbox-recipes";
// GOOGLE FONTS
googleFontsCache({ cachePrefix: "wb6-gfonts" });
// CONTENT
imageCache({ maxEntries: 10 });
src/service-worker.js
Simplest offline fallback
import { offlineFallback } from 'workbox-recipes'
import { precacheAndRoute } from 'workbox-precaching'
// Include offline.html, offline.png in the WB manifest
precacheAndRoute(self.__WB_MANIFEST)
// Serves a precached web page, or image
// if there's neither connection nor cache hit
offlineFallback({
pageFallback: "offline.html",
imageFallback: "offline.png"
});
src/service-worker.js
Demo
-
Background Sync API
-
Background Fetch API
-
Native File System API
-
Badging API
-
Contact Picker API
-
Notification Triggers API
Other APIs for offline-ready?
How to extend your SW?
// Adding you own event handlers
self.addEventListener("periodicsync", function (event) {
// Your code
});
// Using existing Workbox plugins
import { BackgroundSyncPlugin } from 'workbox-background-sync';
const bgSyncPlugin = new BackgroundSyncPlugin('myQueue', {
maxRetentionTime: 24 * 60 // Retry for max of 24 Hours
});
// Writing your own Workbox plugins
import { MyBackgroundFetchPlugin } from 'my-background-fetch';
-
Framework-agnostic
-
Rich functionality
-
Maximum flexible configuration
-
Full power of our own service worker
-
Ready for transpilation, tree-shaking, bundling
Set up -> Configure -> Code
Get what you want
-
Demo application
-
Source code
-
Hosted on Azure Static Web Apps
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.
- 3,034