A presentation at Austin JavaScript in in Austin, TX, USA by Dave Rupert
Dave Rupert / @davatron5000
http://shoptalkshow.com
http://godaytrip.com
How many apps does the average mobile user download per month? Percent drop-off for each stage of your mobile app onboarding? Number of companies in the Top 10 app downloads according to COMSCORE? 0 20% 4
Cats vs. Dogs, Right vs. Left, Pants vs. No pants
We received a lot of feedback like “Where can I download it?” and “If it’s not on my homescreen I forget about it.”
Requirement for an app that takes you deep into the woods, it must work deep in the woods.
Alex Russell, Google TL;DR – “The future of the Web hangs on something I invented.”
A background web worker with a few superpowers.
LEAVE THE MAIN THREAD ALONE
Offline
“I don’t have bars, but if I did, they’d be full bars. Everyone knows I have great bars. Full bars. If I ever had less than full bars, I’d remember. But I don’t recall ever having less than full bars.”
Minimum Viable Product: An Offline Service Worker
manifest.json offline.html service-worker.js (HTTPS too)
manifest.json
offline.html
service-worker.js
10x Faster Than React, Angular, Ember, and jQuery Combined!
Install Activate Fetch Sync Push
self.addEventListener(‘install’, function( event ) { event.waitUntil( // // Prime the caches // ) });
“Hey event, wait until the stuff in these parenthesis are done before you tell the other events you fired.”
self.addEventListener(‘activate’, function( event ) { event.waitUntil( // // Clear out old caches // ) });
self.addEventListener(‘fetch’, function( event ) { event.respondWith( // // Instead of fetching stuff from the network, // try this stuff instead. // ) });
“Hey (fetch) event, respond with this instead of what you were gonna do.”
Quite literally one of the hardest problems in Computer Science. But at least we get to, you know, use JavaScript to do it.
caches.open( cacheName ) caches.keys() caches.match( request ) caches.delete( key )
~100 Lines of Fun, Stolen from adactio.com/serviceworker.js
var staticCacheName = ‘static’; var version = ‘v1::’;
function updateStaticCache() { return caches.open(version + staticCacheName) .then(function (cache) { return cache.addAll([ ‘/assets/application.css’ ‘/assets/application.js’, ‘/offline’ ]); }); };
self.addEventListener(‘install’, function(event) { event.waitUntil( updateStaticCache() ); });
self.addEventListener(‘activate’, function (event) { event.waitUntil( caches.keys().then(function (keys) { // Remove caches whose name is no longer valid return Promise.all( keys.filter(function (key) { return key.indexOf(version) !== 0; }) .map(function (key) { return caches.delete(key); }) ); })); });
self.addEventListener(‘fetch’, function (event) { // Step 1: Always fetch non-GET requests from the network // Step 2: For TEXT/HTML do this: // a) Try the network first // b) If that fails, fallback to the cache // c) If that doesn’t exist, show the offline page // Step 3: For non-TEXT/HTML (e.g. Images) do this: // a) Try the cache first // b) If that fails, try the network // c) If that fails, hijack the request });
// Step 1: Always fetch non-GET requests from the network var request = event.request; if (request.method !== ‘GET’) { event.respondWith( fetch(request) .catch(function () { return caches.match(‘/offline’); }) ); return; }
// Step 2 For TEXT/HTML do this… if (request.headers.get(‘Accept’).indexOf(‘text/html’) !== -1) { event.respondWith( fetch(request) // Then Stuff // Catch Stuff ); return; }
// Step 2: Then Stuff… .then(function (response) { // Stash a copy of this page in the cache var copy = response.clone(); caches.open(version + staticCacheName) .then(function (cache) { cache.put(request, copy); }); return response; })
// Step 2: Catch Stuff… .catch(function () { return caches.match(request).then(function (response) { return response || caches.match(‘/offline’); }) })
// Step 3: For non-TEXT/HTML (e.g. Images) do this… event.respondWith( caches.match(request).then(function (response) { return response || fetch(request) .catch(function () { // If the request is for an image, show an offline placeholder if (request.headers.get(‘Accept’).indexOf(‘image’) !== -1) { return new Response(‘<svg>…</svg>’, { headers: { ‘Content-Type’: ‘image/svg+xml’ } }); } }); }) );
The Last Step?
if (‘serviceWorker’ in navigator) { navigator.serviceWorker.register( ‘/service-worker.js’, { scope: ‘/’ }).then(function(reg) { console.log(‘Works! Scope is ’ + reg.scope); }).catch(function(error) { console.log(‘Failed with ’ + error); }); }
Browser Cache With Service Worker
2 minute session time
~5 minutes apart Depends on browser’s own heuristics.
Asset pipelines, digest fingerprinting, and scope issues.
function updateStaticCache() { return caches.open(version + staticCacheName) .then(function (cache) { return cache.addAll([ ‘<%= url_to_stylesheet “application” %>’ ‘<%= url_to_javascript “application” %>’, ‘/offline’ ]); }); };
Nope Yep • assets/ • assets/ • serviceworker.js • logo.png • index.html • offline.html • manifest.json • logo.png • index.html • offline.html • manifest.json • serviceworker.js
• Fixes Scope Issues • Sets up route to Dynamic Service Worker in the root • Sets appropriate Expires headers
The… ahem… intricacies… of building with Service Workers
localhost:4000 localhost:4000
Enterprise Ruby on Rails, Finally!
@davatron5000
A case-study talk on how we made DayTrip, an app that helps you leave your house on the weekends, into a minimum viable Progressive Web App with an offline service worker.
This talk was originally given at Austin JavaScript on a rainy, haunted night.