Maxim Salnikov
@webmaxru
For Production
In 1-2-3
Part3
Maxim Salnikov
@webmaxru
For Production
In 1-2-3
Part 2
Permanent link:
If you want to code during the session
We need a laptop/desktop with installed (latest stable versions):
- Git
- Node
- NPM
Browsers (latest stable versions):
- Chrome / Firefox / Edge
Quick start
npm install serve -g
git clone https://github.com/webmaxru/pwa-for-production
cd pwa-for-production
git checkout part-1
npm install
serve
Recommended to copy code snippets from
Optional
Maxim Salnikov
-
PWA Slack organizer
-
PWA Oslo / PWA Oslo meetups organizer
-
PWA speaker and trainer
Full-Stack Developer, PWAdvocate
Agenda
-
What are the cons and challenges of the manually written service workers
-
Introducing Workbox
-
Automating application shell lifecycle: from building to serving
-
Organizing a proper UX for the app update
-
Runtime caching: strategies
-
Proper registration of Workbox in your app
=
+
Application shell
Web App Manifest
Fast, responsive, mobile-first
Served via HTTPS
Application UI (App Shell)
Let's build an App shell
My App
-
Define the minimal set of resources we need, and keep versioning in mind
-
On the first app load, explicitly put these resources into the Cache Storage
-
On the next app loads, serve resources from this cache before going to the network
-
At the same time, check if the never version is available. If yes, update the cache
Logically
Physically
-file(s)
App
Service-worker
Browser/OS
Event-driven worker
Cache
fetch
push
sync
Service Worker 101
Managing cache
self.addEventListener('install', event => {
// Use Cache API to cache html/js/css
})
self.addEventListener('activate', event => {
// Clean the cache from the obsolete app shell versions
})
handmade-service-worker.js
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 (for API?)
)
} else {
event.respondWith(
// Cache-First Strategy (for app shell?)
)
}
})
handmade-service-worker.js
In the real world
-
All kinds of fallbacks needed for the strategies
-
There are more complex strategies like Stale-While-Revalidate
-
Good to have some form of the routing
-
Good to have the possibility to provide some extra settings for different resource groups
-
...
-
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
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/',
globPatterns: [
'**/*.{txt,png,ico,html,js,json,css}'
],
swSrc: 'src/workbox-service-worker.js',
swDest: 'dist/sw.js'
}
workbox-build-inject.js
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/workbox-service-worker.js
Caching, serving, managing versions
Precaching manifest
[
{
"url": "index.html",
"revision": "34c45cdf166d266929f6b532a8e3869e"
},
{
"url": "favicon.ico",
"revision": "b9aa7c338693424aae99599bec875b5f"
},
...
]
Build flow integration
{
"scripts": {
"build-pwa": "npm run build-app &&
node workbox-build-inject.js"
}
}
package.json
Coding time
Application Data
Intercepting requests
self.addEventListener('fetch', event => {
if (event.request.url.indexOf('/api/breakingnews') != -1) {
event.respondWith(
// Network-First Strategy
)
} else if (event.request.url.indexOf('/api/archive') != -1 {
event.respondWith(
// Cache-First Strategy
)
}
})
handmade-service-worker.js
Routes and strategies
workbox.routing.registerRoute(
new RegExp('/api/breakingnews'),
new workbox.strategies.NetworkFirst()
);
src/workbox-service-worker.js
workbox.routing.registerRoute(
new RegExp('/api/archive'),
new workbox.strategies.CacheFirst({
plugins: [...]
})
);
Strategies
-
CacheFirst
-
CacheOnly
-
NetworkFirst
-
NetworkOnly
-
StaleWhileRevalidate
Plugins
-
Expiration
-
CacheableResponse
-
BroadcastUpdate
-
BackgroundSync
-
...your own plugin?
Coding time
Better UX for update flow
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
main.js
workbox.precaching.addPlugins([
new workbox.broadcastUpdate.Plugin('app-shell')
]);
src/workbox-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
Coding time
In the next session...
All beyond caching: web push notifications, background sync/fetch, upcoming APIs
-
Intro to push notifications, required parties, flow
-
Important notice on notifications UX
-
The latest stable PWA API for large resources downloading: background fetch
-
APIs of the Project Fugu: let's code something that will be only available soon!
-
Course summary: testing your PWA in Lighthouse
-
Other famous PWA case studies for your inspiration
Thank you!
Maxim Salnikov
@webmaxru
Questions?
Maxim Salnikov
@webmaxru
PWA For Production In 1-2-3. Part 2
By Maxim Salnikov
PWA For Production In 1-2-3. Part 2
We take the latest version of the awesome Workbox library to see how easy is to automate all the offline-ready functionality we need for PWA while still keeping a full control on the service worker behavior. Agenda: What are the cons and challenges of the manually written service workers Introducing Workbox Automating application shell lifecycle: from building to serving Organizing a proper UX for the app update Runtime caching: strategies What is background sync and how to implement it using Workbox Proper registration of Workbox in your app
- 3,871