A Pragmatist's Guide to Service Workers
Lyza Danger Gardner
@lyzadanger | www.lyza.com
Smashing Freiburg 2016
Pragmatist's
Service Workers!
Service Workers!
...what?
A service worker is a script
It acts as a proxy
OK...
but why?
You can decide what happens offline
Boost (online) web perf!
MOAR goodies
SW can be pretty chill
If you follow some rules
Some of those rules...
There will be JS
https://github.com/lyzadanger/pragmatist-service-worker
http://bit.ly/pragmatist-sw
A few reasons to be horrified
Is Service Worker Ready?
Browser Support
New-ish JS
Break it down for me
Service Worker API "Family"
global context
Service Worker's world
SW Mission #1
Provide an Offline Message
What we're up to
Text
Registering a service worker
Register SW from Client Code
Inside the client code
serviceWorker.register(...)
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js');
}
</script>
index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Service Workers: Offline Message</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/default.css" rel="stylesheet" title="Default Style">
</head>
<body>
<p>If you see this, you are online</p>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js');
}
</script>
</body>
</html>
once registered...
Service worker can listen for fetch events
Inside service worker code
Add a fetch handler
self.addEventListener(
);
service-worker.js
self.addEventListener('fetch',
);
self.addEventListener('fetch', event => {
});
Cracking open the event object
Add a fetch handler
self.addEventListener('fetch', event => {
});
service-worker.js
if (event.request.mode === 'navigate') {
}
event.respondWith(/* ... */);
fetch
Add a fetch handler
self.addEventListener('fetch', event => {
});
service-worker.js
if (event.request.mode === 'navigate') {
}
event.respondWith( ));
event.respondWith(fetch(event.request));
The happy path
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(fetch(event.request));
}
});
service-worker.js
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(fetch(event.request));
}
});
service-worker.js
THIS ACCOMPLISHES NOTHING
event.respondWith(fetch(event.request));
fetch(event.request)
fetch(event.request)
fetch returns a Promise
Promises
Settling fetch Promises
Handling failures
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
}
});
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
);
}
});
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
);
}
});
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(error => {
})
);
}
});
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(error => {
return new Response();
})
);
}
});
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(error => {
return new Response('<p>Oh, dear.</p>',
{ headers: { 'Content-Type': 'text/html' } });
})
);
}
});
Mission...complete?
Requests and Responses
request.mode
request.mode
if (event.request.mode === 'navigate')
service-worker.js
if (event.request.mode === 'navigate' ||
(event.request.method === 'GET' &&
if (event.request.mode === 'navigate' ||
(event.request.method === 'GET' &&
event.request.headers.get('accept').includes('text/html')))
Request
Response
event.respondWith(
/* Promise resolving to a Response, ideally */
);
event.respondWith(
fetch(request)
);
event.respondWith(
fetch(request).catch(error => { })
);
event.respondWith(
fetch(request).catch(error => {
return new Response(...);
})
);
event.respondWith(
fetch(request).catch(error => {
return new Response('<p>Oh, dear.</p>');
})
);
event.respondWith(
fetch(request).catch(error => {
return new Response('<p>Oh, dear.</p>',
{
headers: { 'Content-Type': 'text/html' }
}
);
})
);
Something better...
SW Mission #2
Provide an Offline PAGE
Install Phase
service-worker.js
Install handler
self.addEventListener('install', event => {
});
var offlineURL = 'offline.html';
event.waitUntil(
/* Gotta stick offline page in cache */
);
Extendable Event
self.addEventListener('install', event => {
var offlineURL = 'offline.html';
event.waitUntil(
fetch(...).then(...)
);
});
service-worker.js
self.addEventListener('install', event => {
var offlineURL = 'offline.html';
event.waitUntil(
fetch(...).then(...)
);
});
self.addEventListener('install', event => {
var offlineURL = 'offline.html';
event.waitUntil(
fetch(new Request(offlineURL)).then(...)
);
});
self.addEventListener('install', event => {
var offlineURL = 'offline.html';
event.waitUntil(
fetch(new Request(offlineURL)).then(response => {
})
);
});
CacheStorage
CacheStorage interface
caches.open()
self.addEventListener('install', event => {
var offlineURL = 'offline.html';
event.waitUntil(
fetch(new Request(offlineURL)).then(response => {
})
);
});
service-worker.js
return caches.open('offline').then(cache => {
});
cache.put(...)
self.addEventListener('install', event => {
var offlineURL = 'offline.html';
event.waitUntil(
fetch(new Request(offlineURL)).then(response => {
return caches.open('offline').then(cache => {
});
})
);
});
service-worker.js
return cache.put(offlineURL, response);
update the fetch handler...
self.addEventListener('fetch', event => {
var request = event.request;
if (request.mode === 'navigate' ||
(request.method === 'GET' &&
request.headers.get('accept').includes('text/html'))) {
event.respondWith(
);
}
});
service-worker.js
Updated fetch handler
self.addEventListener('fetch', event => {
var request = event.request;
if (request.mode === 'navigate' ||
(request.method === 'GET' &&
request.headers.get('accept').includes('text/html'))) {
event.respondWith(
fetch(request)
})
);
}
});
self.addEventListener('fetch', event => {
var request = event.request;
if (request.mode === 'navigate' ||
(request.method === 'GET' &&
request.headers.get('accept').includes('text/html'))) {
event.respondWith(
fetch(request).catch(error => {
})
);
}
});
self.addEventListener('fetch', event => {
var request = event.request;
if (request.mode === 'navigate' ||
(request.method === 'GET' &&
request.headers.get('accept').includes('text/html'))) {
event.respondWith(
fetch(request).catch(error => {
return caches.open('offline')
})
);
}
});
self.addEventListener('fetch', event => {
var request = event.request;
if (request.mode === 'navigate' ||
(request.method === 'GET' &&
request.headers.get('accept').includes('text/html'))) {
event.respondWith(
fetch(request).catch(error => {
return caches.open('offline').then(cache => {
});
})
);
}
});
self.addEventListener('fetch', event => {
var request = event.request;
if (request.mode === 'navigate' ||
(request.method === 'GET' &&
request.headers.get('accept').includes('text/html'))) {
event.respondWith(
fetch(request).catch(error => {
return caches.open('offline').then(cache => {
return cache.match('offline.html');
});
})
);
}
});
cache.match(...)
event.respondWith(
fetch(request).catch(error => {
})
);
service-worker.js
Updated fetch handler
return caches.open('offline').then(cache => {
});
return caches.open('offline').then(cache => {
return cache.match('offline.html');
});
RESULT
MOAR about Registration
Inside the client code
Registration
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js');
}
index.html
navigator
register
feature testing
service worker file location
Scope
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js'
);
}
index.html
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js',
{ scope : './' }
);
}
Service worker can listen for fetch events
SW Scope
SW Mission #3
Network Strategies
Fielding fetches
Handling different requests
self.addEventListener('fetch', event => {
var request = event.request;
});
service-worker.js
if (isNavigateRequest(request)) {
}
// Handle request for HTML: network-first
} else if (isImageRequest(request)) {
// Handle request for images: cache-first
}
self.addEventListener('fetch', event => {
var request = event.request;
if (isNavigateRequest(request)) {
// Handle request for HTML: network-first
} else if (isImageRequest(request)) {
// Handle request for images: cache-first
}
});
function isNavigateRequest (request) {
return (request.mode === 'navigate' ||
(request.method === 'GET' &&
request.headers.get('accept').includes('text/html')));
}
function isImageRequest (request) {
return (request.headers.get('Accept').indexOf('image') !== -1);
}
Content: network-first strategy
Keeping a copy around
Handling content requests
if (isNavigateRequest(request)) {
}
service-worker.js
event.respondWith(
fetch(request)
);
.then(response => addToCache(request, response))
Adding to Cache
Read-through caching
function addToCache (request, response) {
}
service-worker.js
// You do not want to cache a bad response!
if (response.ok) {
}
// For Promise chaining
return response;
// A Response can only be "used" once
const copy = response.clone();
caches.open('assets').then(cache => {
cache.put(request, copy);
});
fetch fail
Handling content requests
if (isNavigateRequest(request)) {
}
service-worker.js
event.respondWith(
fetch(request)
);
.then(response => addToCache(request, response))
.catch(() => fetchFromCache(request))
Fetching from cache
function fetchFromCache (request) {
}
service-worker.js
return caches.match(request).then(response => {
});
caches.match(...)
Fetching from cache
function fetchFromCache (request) {
}
service-worker.js
return caches.match(request)
);
if (!response) {
throw Error(`${request.url} not found in cache`);
}
return response;
return caches.match(request).then(response => {
});
caches.match fail
Handling content requests
if (isNavigateRequest(request)) {
event.respondWith(
fetch(request)
.then(response => addToCache(request, response))
.catch(() => fetchFromCache(request))
.catch(() => offlinePage())
);
}
service-worker.js
function offlinePage () {
return caches.open('offline').then(cache => {
return cache.match('offline.html');
});
}
Handling image requests
else if (isImageRequest(request)) {
event.respondWith(
);
}
service-worker.js
fetchFromCache(request) // Cache first
.catch(() => fetch(request) // But if not in cache, fetch
// If fetch was successful, add to cache for later
.then(response => addToCache(request, response)))
// If fetch failed...
.catch(() => console.log('unable to respond to request'))
The Life of a Service Worker
SW Lifecycle
skipWaiting
SW Mission #4
Application Shell
service-worker.js
URLs to Cache
const cacheFiles = [
'',
'default.css',
'static-assets/cloud-1.jpg',
'static-assets/cloud-2.jpg',
'static-assets/cloud-3.jpg',
'static-assets/cloud-4.jpg',
'static-assets/cloud-5.jpg',
'static-assets/cloud-6.jpg',
'static-assets/cloud-7.jpg',
'static-assets/cloud-8.jpg',
'static-assets/cloud-9.jpg',
'static-assets/cloud-10.jpg'
];
Caching Shell Assets
service-worker.js
self.addEventListener('install', event => {
event.waitUntil(
);
});
self.addEventListener('install', event => {
event.waitUntil(
caches.open('shell').then(cache => {
})
);
});
cache.addAll(...)
Caching Shell Assets
service-worker.js
self.addEventListener('install', event => {
event.waitUntil(
caches.open('shell').then(cache => {
})
);
});
self.addEventListener('install', event => {
event.waitUntil(
caches.open('shell').then(cache => {
return cache.addAll(cacheFiles);
})
);
});
self.addEventListener('install', event => {
event.waitUntil(
caches.open('shell').then(cache => {
return cache.addAll(cacheFiles);
}).then(() => self.skipWaiting())
);
});
Straight to Activate
Fetch network strategy
index.html
self.addEventListener('fetch', event => {
var request = event.request;
});
self.addEventListener('fetch', event => {
var request = event.request;
var url = new URL(request.url);
});
self.addEventListener('fetch', event => {
var request = event.request;
var url = new URL(request.url);
if (cacheFiles.indexOf(url.pathname) !== -1) {
}
});
self.addEventListener('fetch', event => {
var request = event.request;
var url = new URL(request.url);
if (cacheFiles.indexOf(url.pathname) !== -1) {
event.respondWith(
fetchFromCache(request)
);
}
});
self.addEventListener('fetch', event => {
var request = event.request;
var url = new URL(request.url);
if (cacheFiles.indexOf(url.pathname) !== -1) {
event.respondWith(
fetchFromCache(request)
.catch(() => fetch(request))
);
}
});
Offline action!
SW Mission #5
Versioning and Cleanup
Remember activate?
activate event
cache cleanup
const cachePrefix = 'mission-05';
service-worker.js
Cache prefixing
caches.keys()
self.addEventListener('activate', event => {
event.waitUntil(
);
});
service-worker.js
Activate handler
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheKeys => {
})
);
});
Filtering and deleting caches
service-worker.js
Activate handler
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheKeys => {
})
);
});
var oldCacheKeys = cacheKeys.filter(key => {
return (key.indexOf(cachePrefix) !== 0);
});
var deletePromises = oldCacheKeys.map(oldKey => {
return caches.delete(oldKey);
});
return Promise.all(deletePromises);
service-worker.js
Activate handler
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheKeys => {
var oldCacheKeys = cacheKeys.filter(key => {
return (key.indexOf(cachePrefix) !== 0);
});
var deletePromises = oldCacheKeys.map(oldKey => {
return caches.delete(oldKey);
});
return Promise.all(deletePromises);
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheKeys => {
var oldCacheKeys = cacheKeys.filter(key => {
return (key.indexOf(cachePrefix) !== 0);
});
var deletePromises = oldCacheKeys.map(oldKey => {
return caches.delete(oldKey);
});
return Promise.all(deletePromises);
}).then(() => self.clients.claim())
);
});
You made it!
But, Wait.
There's more!
Getting Fancy Pants
Offline images and more
MOAR Resources
- MDN's Using Service Workers
- The Service Worker Cookbook at serviceworke.rs
- Is Service Worker Ready?
- Some of Chrome's Feature Examples
http://bit.ly/pragmatist-sw
All the Examples
All the Slides
http://bit.ly/pragmatist-sw-slides
@lyzadanger | lyza.com
The Pragmatist's Guide to Service Workers
By lyzadanger
The Pragmatist's Guide to Service Workers
- 1,457