https://slides.com/agesteira/pwas-with-angular-and-workbox
Twitter: Andres Gesteira (@GeorgeKaplan_G)
Loves 70s and 80s films.
Senior Product Engineer
Angular Developer
Lives in Berlin
What makes a Web App "progressive"?
It loads even when we are offline => App Shell
It is installable => Web App Manifest
It is cross-platform => Also desktop!
App Shell
It is the minimal HTML, CSS and JS to power the user interface.
You can think of it as the substitute of the SDK in a mobile context.
This approach relies on aggressively precaching files.
It shows the First Contentful Paint
The magic technology to do that is called Service Workers.
User Centric Metrics
From a user's perception page loads also have lifecycles
Text
Screenshot from the Google I/O 2017 conference.
The App Manifest
A PWA is installable if the platform has support for this:
<link rel="manifest" href="/manifest.json">
{
"short_name": "Maps",
"name": "Google Maps",
"icons": [
{
"src": "/images/icons-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/images/icons-512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "/?launch=pwa",
"background_color": "#3367D6",
"display": "standalone",
"orientation": "landscape",
"scope": "/maps/",
"theme_color": "#3367D6"
}
JavaScript Threads
`window` in a browser.
`global` in Node.js
`self` in workers.
* If you just want to get the global object regardless of the context you need to use the `globalThis` property.
Workers
Web Workers: they offload heavy processing from the main thread.
*Worklets: they give access to low-level parts of the rendering pipeline.
Service Workers: event driven workers that act as a proxy servers.
* Houdini uses the `PaintWorklet` under the hood.
*Service Workers...
...are a replacement for the deprecated Application Cache.
...follow the the Extensible Web Manifesto philosophy.
...offer offline capabilities such as Push Notifications and Background Sync.
* Check out Jake Archibald's site about Service Workers.
...are request interceptors either to the Network or to the Cache Storage.
...only run over HTTPS or http://localhost, for security reasons.
Service Workers Lifecycle
Ensures that the page is controlled by only 1 version of the Service Worker.
It is made of 3 events:
It can have 2 possible scenarios:
A. Newly created Service Worker.
B. Updated Service Worker.
What is Workbox?
We register our Service Worker as normal...
...but in the worker thread we can start using the libraries:
It is a set of libraries that simplifies that process.
importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');
if (workbox) {
console.log(`Yay! Workbox is loaded 🎉`);
} else {
console.log(`Boo! Workbox didn't load 😬`);
}
Precaching
Wraps all 3 service worker lifecycle events with very few lines of code:
workbox.precaching.precacheAndRoute([
'/styles/index.0c9a31.css',
'/scripts/main.0d5770.js',
{ url: '/index.html', revision: '383676' },
]);
Runtime Caching
This is what we natively did with the fetch event but now using Workbox:
workbox.routing.registerRoute(
'/logo.png',
handler
);
Common Runtime Caching Strategies
The Workbox Strategies package handles the most common scenarios:
Check out the Rick And Morty PWA
A framework-agnostic workshop:
Add a Service Worker
On an existing Angular project.
ng add @angular/pwa --project *project-name*
* Project flag is only necessary for non-default apps.
*
npm i lite-server -D
ng build --prod
npx lite-server --baseDir="dist/app-name"
Changes: package.json
@angular/pwa is an schematics package.
@angular/service-worker: ServiceWorkerModule, SwUpdate & SwPush.
lite-server is good for serving SPAs.
Changes: manifest.webmanifest (I)
The extension is irrelevant.
*.webmanifest is more convenient for some server configs.
Changes: manifest.webmanifest (II)
Changes: app.module.ts
Changes: ngsw-config.json (I)
Changes: ngsw-config.json (II)
dataGroups
export interface DataGroup {
name: string;
urls: string[];
version?: number;
cacheConfig: {
maxSize: number;
maxAge: string;
timeout?: string;
strategy?: 'freshness' | 'performance';
};
}
freshness === Network First falling back to cache
performance === Cache First falling back to network
Build
Serve
Initial settings
On an existing Angular project.
npm i lite-server lighthouse workbox-cli -D
"scripts": {
"build": "ng build --prod",
"lighthouse": "npx lighthouse http://localhost:3000/ --view --chrome-flags=\"--headless\"
--output-path=./lighthouse/lighthouse_\"$(date \"+%Y-%m-%d_%H-%M-%S\")\".report.html",
"lint": "ng lint",
"ng": "ng",
"serve:dev": "ng serve",
"serve:prod": "npx lite-server --baseDir=\"dist/app-name\"",
"start": "npm run serve:dev"
},
# .gitignore
/lighthouse/**.report.html
npm run build
npm run serve:prod
npm run lighthouse
Go offline => Dino Game
manifest.json
Generate Manifest with https://app-manifest.firebaseapp.com/
Copy icons folder to assets folder.
Copy generated file to src/manifest.json
Modify src/manifest.json.
Add src/manifest as an asset in angular.json.
index.html
Add meta tags:
Add link tags:
Add noscript tag:
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Manifest name property" />
<meta name="description" content="Description" />
<meta name="theme-color" content="#xxx" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/assets/img/icons/angular-logo-512x512.png" />
<noscript>Please enable JavaScript to continue using this application.</noscript>
npm run build
npm run lighthouse
Add Service Worker (I)
Extend build script in package.json:
Create src/sw-custom.js:
"... && npx workbox copyLibraries dist/app-name && npx workbox injectManifest",
importScripts('/workbox-vx.x.x/workbox-sw.js');
if (workbox) {
console.log(`Yay! Workbox is loaded 🎉`);
workbox.setConfig({
modulePathPrefix: '/workbox-vx.x.x/'
});
workbox.precaching.precacheAndRoute([]);
workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL('/index.html'));
} else {
console.log(`Boo! Workbox didn't load 😬`);
}
Add Service Worker (II)
Create workbox-config.js:
module.exports = {
globDirectory: 'dist/app-name',
globPatterns: ['**/*.{txt,ttf,otf,css,png,ico,html,js,json,mjs}'],
swDest: 'dist/app-name/sw.js',
swSrc: 'src/sw-custom.js',
globIgnores: ['workbox-vx.x.x/**/*']
};
Add Service Worker (III)
Register Service Worker in main.ts
platformBrowserDynamic()
.bootstrapModule(AppModule)
.then(() => {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(
registration => {
console.log(`Service Worker registered! Scope: ${registration.scope}`);
},
error => {
console.error(`Service Worker registration failed: ${error}`);
}
);
});
}
})
.catch(err => console.error(err));
npm run build
npm run lighthouse
Go offline => We keep our app
Install and uninstall
Warning
This implementation is going to change with Workbox 5.
NGSW
Easy to start.
Seamless integration with Angular (SwUpdate, SwPush).
Includes Safety Worker.
Only allows 2 runtime caching strategies.
We cannot access the service worker.
Workbox
Full power of your own Service Worker.
Maximum flexibility with configurations.
Rich functionality for professional PWA development.
It requires a better understanding of Service Workers.
We lose @angular/service-worker features so we need to write them.
A series of articles coming soon
Workbox 5 implementation.
How to replace SwUpdate & SwPush.
How to set caching routes and debug.
And much more.
Andres Gesteira @GeorgeKaplan_G