Valentin Goșu
valentin.gosu@gmail.com
Â
Offline
Performance
Push & Notifications
DEPRECATED!
CACHE MANIFEST
# 2010-06-18:v3
# Explicitly cached entries
index.html
css/style.css
# offline.html will be displayed if the user is offline
FALLBACK:
/ /offline.html
# All other resources (e.g. sites) require the user to be online.
NETWORK:
*
# Additional resources to cache
CACHE:
images/logo1.png
images/logo2.png
images/logo3.png
"Application Cache is a Douchebag"
Exception: localhost
Try letsencrypt.org
Github Pages available over HTTPS
'use strict';
if (window.location.hostname != 'localhost' && window.location.protocol == 'http:') {
// If we're on github, make sure we're on https
window.location.protocol = "https";
}
'use strict';
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('serviceWorker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
console.log('ServiceWorker registration failed: ', err);
});
}
/hello-world/hello.js
Register the service worker
var promise = new Promise(function(resolve, reject) {
resolve(1);
});
promise.then(function(val) {
console.log(val); // 1
return val + 2;
}).then(function(val) {
console.log(val); // 3
});
Promises
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
});
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
});
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
});
'use strict';
self.addEventListener('install', event => {
console.log('service worker - install');
function onInstall () {
return caches.open('static')
.then(cache =>
cache.addAll([
'hello.js', // Using relative path. Could also say /hello-world/hello.js
'index.html'
])
);
}
event.waitUntil(onInstall());
});
self.addEventListener('activate', event => {
console.log('service worker - activate');
});
/hello-world/serviceWorker.js
Â
Handle install event
self.addEventListener('install', event => {
// Skip waiting. Activate immediately.
event.waitUntil(
onInstall() // add statics to cache
// and trigger activate immediately
.then( () => self.skipWaiting() )
);
});
self.addEventListener('activate', event => {
function onActivate () {
// Maybe cleanup old caches
}
event.waitUntil(
onActivate()
// This makes the SW take effect immediately
// on any open pages in scope
.then( () => self.clients.claim() )
);
});
/hello-world/serviceWorker.js
Â
Handle install event - faster
Firefox
about:serviceworkers
Chrome
chrome://serviceworker-internals
Firefox
Devtools > Storage > Cache Storage
Chrome
Devtools > Resources > Cache Storage
self.addEventListener('fetch', event => {
function shouldHandleFetch (event) {
// Should we handle this fetch?
}
function onFetch (event) {
// TODO: Respond to the fetch
}
if (shouldHandleFetch(event)) {
onFetch(event);
}
});
/hello-world/serviceWorker.js
Â
Handle fetch event
function shouldHandleFetch (event) {
// Should we handle this fetch?
var request = event.request;
var url = new URL(request.url);
// Your criteria:
// * what do you want to intercept?
// * can't cache resources from other domains
// We don't want to intercept the fetch of the service worker,
// or we might not get any updates :)
return !(url.href == serviceWorkerAddress ||
(url.hostname != 'localhost' && url.hostname != 'awesome-sw.github.io'));
}
/hello-world/serviceWorker.js
Â
Handle fetch event
function onFetch (event) {
var request = event.request;
var acceptHeader = request.headers.get('Accept');
var resourceType = 'static';
var url = new URL(request.url);
if (acceptHeader.indexOf('text/html') !== -1) {
resourceType = 'content';
} else if (acceptHeader.indexOf('image') !== -1) {
resourceType = 'image';
} else if (url.pathname.indexOf('/generated/') === 0) {
resourceType = 'generated';
}
// respond to fetch according to resourceType
}
/hello-world/serviceWorker.js
Â
Handle fetch event
function onFetch (event) {
// [...]
// respond to fetch according to resourceType
// Strategy:
// * images - cache first
// * content - network first
// * generated - build a response on the fly
// * offline fallback - not in cache, network fails
// * modify response
// * other
}
/hello-world/serviceWorker.js
Â
Handle fetch event
function onFetch (event) {
// [...]
// respond to fetch according to resourceType
// Strategy:
// * images - cache first
// * content - network first
// * generated - build a response on the fly
// * offline fallback - not in cache, network fails
// * modify response
// * other
}
/hello-world/serviceWorker.js
Â
Handle fetch event
event.respondWith(
// Search for the resource in the cache
fetchFromCache(event)
// Not in cache. Hit the network
.catch(() => fetch(request))
// Add response to the cache (and return the response)
.then(response => addToCache(cacheKey, request, response))
// Network also failed. Return a generic offline response
.catch(() => offlineResponse(resourceType))
);
/hello-world/serviceWorker.js
Â
Cache first
event.respondWith(
// Fetch the request from the network
fetch(request)
// Add the response to the cache and return it
.then(response => addToCache(cacheKey, request, response))
// Network failed. Search in the cache.
.catch(() => fetchFromCache(event))
// Cache also failed. Return a generic offline response
.catch(() => offlineResponse(opts))
);
/hello-world/serviceWorker.js
Â
Network first
function addToCache (cacheKey, request, response) {
if (response.ok) {
// We may get a response, that is not OK.
// Such as a 404
// Create a copy of the response to put in the cache
var copy = response.clone();
caches.open(cacheKey).then( cache => {
cache.put(request, copy);
});
// Return the response
return response;
}
// The response is a 404 or other error
}
/hello-world/serviceWorker.js
Â
Cache the responses we get
function fetchFromCache (event) {
// Search for a response in all caches
return caches.match(event.request).then(response => {
if (!response) {
// A synchronous error that will kick off the catch handler
throw Error('${event.request.url} not found in cache');
}
// Return the response
return response;
});
}
/hello-world/serviceWorker.js
Â
Retrieve the resource from the cache
function offlineResponse (resourceType) {
if (resourceType === 'image') {
// synchronously build a response
return new Response(svgImageDefinition,
{ headers: { 'Content-Type': 'image/svg+xml' } }
);
} else if (resourceType === 'content') {
// return another
return caches.match(staticOfflinePage);
}
return undefined;
}
/hello-world/serviceWorker.js
Â
Generate an offline response
var endpoint;
navigator.serviceWorker.register('serviceWorker.js').then(function(registration) {
return registration.pushManager.getSubscription()
.then(function(subscription) {
// If a subscription was found, return it.
if (subscription) {
return subscription;
}
// Otherwise, subscribe the user
return registration.pushManager.subscribe({ userVisibleOnly: false });
});
}).then(function(subscription) {
// This is the URL of the endpoint we need to call to get a notification
endpoint = subscription.endpoint;
});
/hello-world/hello.js
Push & Notifications
self.addEventListener('push', function(event) {
var payload = event.data ? event.data.text() : 'Alea iacta est';
event.waitUntil(
// There are many other possible options, for an exhaustive list see the specs:
// https://notifications.spec.whatwg.org/
self.registration.showNotification('ServiceWorker Cookbook', {
lang: 'la',
body: payload,
icon: 'caesar.jpg',
vibrate: [500, 100, 500],
})
);
})
/hello-world/serviceWorker.js
Push & Notifications
self.addEventListener('push', function(event) {
var payload = event.data ? event.data.text() : 'Alea iacta est';
// may haves several pages and just one serviceWorker
clients.matchAll().then(function(clients){
clients[0].postMesssage(payload);
});
})
/hello-world/serviceWorker.js
Trigger events
navigator.serviceWorker.addEventListener('message', function(e) {
console.log(e.data);
});
/hello-world/hello.js
curl --request POST -H "TTL: 60"
https://updates.push.services.mozilla.com/push/XXX...YYY=
curl
Send a notification (Firefox)
fetch(endpoint, {
method: "POST",
headers: {
"TTL": "60"
}
});
fetch it!
curl --header "Authorization: key=AIzaSyBBh4ddPa96rQQNxqiq_qQj7sq1JdsNQUQ"
--header "Content-Type: application/json"
https://android.googleapis.com/gcm/send
-d "{\"registration_ids\":[\"XXXYYY\"]}"
curl
Send a notification (Chrome)
Run your own server
curl https://serviceworke.rs/push-payload/sendNotification \
-H 'Content-Type: application/json' \
-d '{"endpoint":"https://updates.push.services.mozilla.com/push/...1Kw=",
"key":"B...Yqar9yAQaN9E=","payload":"Insert here a payload","delay":"5","ttl":"0"}'
curl
Send a notification with payload (Firefox)
fetch("https://serviceworke.rs/push-payload/sendNotification", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: '{"endpoint":"https://updates.push.services.mozilla.com/push/g...w=",
"key":"B...E=",
"payload":"Insert here a payload","delay":"5","ttl":"0"}'
}).then(e => console.log(e));
fetch it!
.then(function(subscription) {
// This is the URL of the endpoint we need to call to get a notification
console.log('endpoint: ', subscription.endpoint);
var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
var key = rawKey ?
btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) :
'';
console.log('key: ', key);
});
get the key
Send a notification with payload (Firefox)
self.addEventListener('push', function(event) {
event.waitUntil(
getEndpoint()
.then(function(endpoint) {
return fetch('./getPayload?endpoint=' + endpoint);
})
.then(function(response) {
return response.text();
})
.then(function(payload) {
self.registration.showNotification('ServiceWorker Cookbook', {
body: payload,
});
})
);
});
serviceWorker.js
Send a notification with payload (Chrome)
app.post(route + 'sendNotification', function(req, res) {
setTimeout(function() {
payloads[req.body.endpoint] = req.body.payload;
webPush.sendNotification(req.body.endpoint, req.body.ttl)
.then(function() {
res.sendStatus(201);
});
}, req.body.delay * 1000);
});
app.get(route + 'getPayload', function(req, res) {
res.send(payloads[req.query.endpoint]);
});
./getPayload
Send a notification with payload (Chrome)
Demo