Real World
Service Workers
Simon MacDonald
@macdonst
What is a
service-worker?
A service worker…
-
Is a type of web worker.
-
Intercepts network requests.
-
Caches or retrieves resources from the cache.
-
Delivers push messages.
-
And more…
Installing
Activated
Idle
Fetch/
Message
Error
Terminated
Lifecycle
navigator.serviceWorker.register('/sw.js');
Load sw.js
var CACHE_VERSION = 'v1';
var CACHE_LIST = [
'index.html',
'css/main.css',
'js/main.js'
];
self.addEventListener('install', function(event) {
event.waitUntil(caches.open(CACHE_VERSION)
.then(function(cache) {
return cache.addAll(CACHE_LIST);
}));
});
self.addEventListener('activate', function(event) {
console.log("service worker has been activated.");
});
self.addEventListener('fetch', function(event) {
console.log('Fetching ' + event.request.url);
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
console.log('Found!');
return response;
}
console.log('Fetch from network...');
return fetch(event.request);
})
);
});
Slow Initial App Load
Cache App Shell
var CACHE_VERSION = 'v1';
var CACHE_LIST = ['index.html',
'css/main.css',
'js/main.js'
];
self.addEventListener('install', function(event) {
event.waitUntil(caches.open(CACHE_VERSION)
.then(function(cache) {
return cache.addAll(CACHE_LIST);
}));
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
Network with cache fallback
self.addEventListener("fetch", function(event) {
event.respondWith(
fetch(event.request).catch(function() {
return caches.match(event.request);
})
);
});
No Content
Broken Images
Generic fallback
self.addEventListener("fetch", function(event) {
event.respondWith(
fetch(event.request).catch(function() {
return caches.match(event.request)
.then(function(response) {
return response || caches.match("/image.png");
});
})
);
});
App service-worker Communication
Cache then Network
self.addEventListener('fetch', function(evt) {
evt.respondWith(fromCache(evt.request));
});
function fromCache(request) {
return caches.open(CACHE)
.then(function (cache) {
return caches.match(request)
.then(function (matching) {
return matching
|| Promise.reject('no-match');
});
});
}
Slow Network
self.addEventListener('fetch', function(evt) {
evt.respondWith(fromCache(evt.request));
evt.waitUntil(update(evt.request));
});
function fromCache(request) {
return caches.open(CACHE)
.then(function (cache) {
return caches.match(request)
.then(function (matching) {
return matching
|| Promise.reject('no-match');
});
});
}
function update(request) {
return caches.open(CACHE)
.then(function (cache) {
return fetch(request)
.then(function (response) {
postUpdate(response);
return cache.put(request, response);
});
});
}
function postUpdate(response) {
self.clients.matchAll({ includeUncontrolled: true })
.then(function(clients) {
clients.forEach(function(client) {
client.postMessage(
{action: "update", tabatas: response.json()}
);
});
});
};
// index.js
navigator.serviceWorker
.addEventListener("message", function (event) {
var data = event.data;
if (data.action === "update") {
updateTabatas(data.tabatas);
}
});
Can't Submit Data Offline
Save in IndexDB
// index.js
idb.open('messages', 1, function(upgradeDb) {
upgradeDb.createObjectStore('outbox',
{ autoIncrement : true, keyPath: 'id' });
}).then(function(db) {
var transaction = db.transaction('outbox',
'readwrite');
return transaction
.objectStore('outbox')
.put(tabata);
}).then(function() {
return reg.sync.register('outbox');
});
// sw.js
self.addEventListener('sync', function(event) {
event.waitUntil(
db.transaction('outbox', mode)
.objectStore('outbox')
.then(function(outbox) {
return outbox.getAll();
}).then(function(tabatas) {
// Send Tabatas to server
}).catch(function(err) { console.error(err); });
);
});
// sw.js
self.addEventListener('sync', function(event) {
event.waitUntil(
db.transaction('outbox', mode)
.objectStore('outbox')
.then(function(outbox) {
return outbox.getAll();
}).then(function(tabatas) {
return Promise.all(tabatas.map(function(tabata) {
return fetch('/tabatas', {
method: 'POST',
body: JSON.stringify(tabata),
headers: {…}
}).then(function(response) {
return response.json();
}).then(function(data) {
if (data.result === 'success') {
return store.outbox('readwrite')
.then(function(outbox) {
return outbox.delete(message.id);
});
}
})
}).catch(function(err) { console.error(err); });
);
});
Respond to Sync Event
Send updates to server
var CACHE_NAME = "gih-cache-v5";
var CACHED_URLS = [
// Our HTML
"/index.html",
"/my-account.html",
// Stylesheets
"/css/gih.css",
"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
"https://fonts.googleapis.com/css?family=Lato:300,600,900",
"/js/vendor/progressive-ui-kitt/themes/flat.css",
// JavaScript
"https://code.jquery.com/jquery-3.0.0.min.js",
"/js/app.js",
"/js/offline-map.js",
"/js/my-account.js",
"/js/reservations-store.js",
"/js/vendor/progressive-ui-kitt/progressive-ui-kitt.js",
// Images
"/img/logo.png",
"/img/logo-header.png",
"/img/event-calendar-link.jpg",
"/img/switch.png",
"/img/logo-top-background.png",
"/img/jumbo-background-sm.jpg",
"/img/jumbo-background.jpg",
"/img/reservation-gih.jpg",
"/img/about-hotel-spa.jpg",
"/img/about-hotel-luxury.jpg",
"/img/event-default.jpg",
"/img/map-offline.jpg",
// JSON
"/events.json",
"/reservations.json"
];
var googleMapsAPIJS = "https://maps.googleapis.com/maps/api/js?key="+
"AIzaSyDm9jndhfbcWByQnrivoaWAEQA8jy3COdE&callback=initMap";
self.addEventListener("install", function(event) {
// Cache everything in CACHED_URLS. Installation fails if anything fails to cache
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll(CACHED_URLS);
})
);
});
self.addEventListener("fetch", function(event) {
var requestURL = new URL(event.request.url);
// Handle requests for index.html
if (requestURL.pathname === "/" || requestURL.pathname === "/index.html") {
event.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return cache.match("/index.html").then(function(cachedResponse) {
var fetchPromise = fetch("/index.html").then(function(networkResponse) {
cache.put("/index.html", networkResponse.clone());
return networkResponse;
});
return cachedResponse || fetchPromise;
});
})
);
// Handle requests for my account page
} else if (requestURL.pathname === "/my-account") {
event.respondWith(
caches.match("/my-account.html").then(function(response) {
return response || fetch("/my-account.html");
})
);
// Handle requests for reservations JSON file
} else if (requestURL.pathname === "/reservations.json") {
event.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return fetch(event.request).then(function(networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
}).catch(function() {
return caches.match(event.request);
});
})
);
// Handle requests for Google Maps JavaScript API file
} else if (requestURL.href === googleMapsAPIJS) {
event.respondWith(
fetch(
googleMapsAPIJS+"&"+Date.now(),
{ mode: "no-cors", cache: "no-store" }
).catch(function() {
return caches.match("/js/offline-map.js");
})
);
// Handle requests for events JSON file
} else if (requestURL.pathname === "/events.json") {
event.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return fetch(event.request).then(function(networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
}).catch(function() {
ProgressiveKITT.addAlert(
"You are currently offline. The content of this page may be out of date."
);
return caches.match(event.request);
});
})
);
// Handle requests for event images.
} else if (requestURL.pathname.startsWith("/img/event-")) {
event.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return cache.match(event.request).then(function(cacheResponse) {
return cacheResponse||fetch(event.request).then(function(networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
}).catch(function() {
return cache.match("/img/event-default.jpg");
});
});
})
);
// Handle analytics requests
} else if (requestURL.host === "www.google-analytics.com") {
event.respondWith(fetch(event.request));
// Handle requests for files cached during installation
} else if (
CACHED_URLS.includes(requestURL.href) ||
CACHED_URLS.includes(requestURL.pathname)
) {
event.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return cache.match(event.request).then(function(response) {
return response || fetch(event.request);
});
})
);
}
});
self.addEventListener("activate", function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (CACHE_NAME !== cacheName && cacheName.startsWith("gih-cache")) {
return caches.delete(cacheName);
}
})
);
})
);
});
var createReservationUrl = function(reservationDetails) {
var reservationUrl = new URL("http://localhost:8443/make-reservation");
Object.keys(reservationDetails).forEach(function(key) {
reservationUrl.searchParams.append(key, reservationDetails[key]);
});
return reservationUrl;
};
var postReservationDetails = function(reservation) {
self.clients.matchAll({ includeUncontrolled: true }).then(function(clients) {
clients.forEach(function(client) {
client.postMessage(
{action: "update-reservation", reservation: reservation}
);
});
});
};
var syncReservations = function() {
return getReservations("idx_status", "Sending").then(function(reservations) {
return Promise.all(
reservations.map(function(reservation) {
var reservationUrl = createReservationUrl(reservation);
return fetch(reservationUrl).then(function(response) {
return response.json();
}).then(function(newReservation) {
return updateInObjectStore(
"reservations",
newReservation.id,
newReservation
).then(function() {
postReservationDetails(newReservation);
});
});
})
);
});
};
self.addEventListener("sync", function(event) {
if (event.tag === "sync-reservations") {
event.waitUntil(syncReservations());
}
});
self.addEventListener("message", function(event) {
var data = event.data;
if (data.action === "logout") {
self.clients.matchAll().then(function(clients) {
clients.forEach(function(client) {
if (client.url.includes("/my-account")) {
client.postMessage(
{action: "navigate", url: "/"}
);
}
});
});
}
});
self.addEventListener("push", function(event) {
var data = event.data.json();
if (data.type === "reservation-confirmation") {
var reservation = data.reservation;
event.waitUntil(
updateInObjectStore(
"reservations",
reservation.id,
reservation)
.then(function() {
return self.registration.showNotification("Reservation Confirmed", {
body: "Reservation for "+reservation.arrivalDate+" has been confirmed.",
icon: "/img/reservation-gih.jpg",
badge: "/img/icon-hotel.png",
tag: "reservation-confirmation-"+reservation.id,
actions: [
{action: "details", title:"Show reservations", icon:"/img/icon-cal.png"},
{action: "confirm", title:"OK", icon:"/img/icon-confirm.png"},
],
vibrate:[500,110,500,110,450,110,200,110,170,40,450,110,200,110,170,40,500]
});
})
);
}
});
self.addEventListener("notificationclick", function(event) {
event.notification.close();
if (event.action === "details") {
event.waitUntil(
self.clients.matchAll().then(function(activeClients) {
if (activeClients.length > 0) {
activeClients[0].navigate("http://localhost:8443/my-account");
} else {
self.clients.openWindow("http://localhost:8443/my-account");
}
})
);
}
});
Workbox Example
importScripts('…/workbox-sw.dev.v2.0.3.js');
const workboxSW = new WorkboxSW({clientsClaim: true});
workboxSW.precache([{
url: 'precached.txt',
revision: '43011922c2aef5ed5ee3731b11d3c2cb',
}]);
workboxSW.router.registerRoute(
/\.txt$/,
workboxSW.strategies.networkFirst()
);
workboxSW.router.registerRoute(
'https://httpbin.org/delay/(.*)',
workboxSW.strategies.networkFirst({networkTimeoutSeconds: 3})
);
workboxSW.router.registerRoute(
'https://httpbin.org/image/(.*)',
workboxSW.strategies.cacheFirst({
cacheName: 'images',
cacheExpiration: { maxAgeSeconds: 7 * 24 * 60 * 60 },
cacheableResponse: {statuses: [0, 200]},
})
);
Resources
- Building Progressive Web Apps by Tal Ater (book)
- Workbox documentation
- ServiceWorker Cookbook
The End
Real World Service Workers
By Simon MacDonald
Real World Service Workers
- 2,723