Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Pragmatist's Guide to Service Worker: Smashing ...

Pragmatist's Guide to Service Worker: Smashing NYC 2017

Service Workers are buzzy and exciting, a collection of Web APIs that do all sorts of handy stuff. Yet you’re a busy web dev. You’ve got sites to build, web apps to code. Let’s decode the complexities of Service Worker capabilities, breaking them down into concepts, recipes and examples.

We’ll get you ready to build real-life useful stuff without the need for an overwhelming or distracting investment of time and energy.

Lyza Gardner

June 20, 2017
Tweet

More Decks by Lyza Gardner

Other Decks in Technology

Transcript

  1. Pragmatist’s Guide to Service Worker I am Lyza Danger Gardner

    I am an Open Web Engineer at Bocoup I can be found at @lyzadanger and https://www.lyza.com Smashing Conference NYC 2017
  2. • Consists of a script (JavaScript file) • Acts as

    a proxy between client (browser) and network A Service Worker: …and quite a lot more. • It can: • define and handle offline experiences • improve online web performance…
  3. • Consists of a script (JavaScript file) • Acts as

    a proxy between client (browser) and network • It can: • define and handle offline experiences • improve online web performance… A Service Worker: …and quite a lot more. with regards to this…
  4. • An object created from within a context • Consists

    of a JavaScript file • The code in that JavaScript file is run in a worker thread • A web worker has a different global context A Web Worker:
  5. • An object created from within a context • Consists

    of a JavaScript file • The code in that JavaScript file is run in a worker thread • A web worker has a different global context A Web Worker: ??? ???
  6. We’ll create a JavaScript file for a Service Worker that

    will listen for fetch events and…do nothing for now.
  7. self.addEventListener('fetch', event => { // Do something about it });

    service-worker.js Adding an event listener for ‘fetch’ events
  8. We need to add some JavaScript to a web page

    to register the Service Worker. <script> //… </script> index.html service-worker.js
  9. Is a path or a pattern, relative to the service

    worker script’s location, you know, e.g.: ./ ./foo ./foo/bar/baz Scope:
  10. I want to register the service worker located at service-worker.js

    against the scope ./blog index.html <script> //… </script>
  11. I want to register the service worker located at service-worker.js

    against the scope ./blog index.html blog service-worker.js
  12. <!doctype html> <html> <head> <!-- ... --> </head> <body> <script>

    if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js'); } </script> </body> </html> index.html Register a service worker
  13. navigator.serviceWorker …provides an object…including facilities to register, unregister and update

    service workers, and access the state of service workers and their registrations. Sincerely, The Service Worker Specification [ServiceWorkerContainer]
  14. We’ll extend our Service Worker so that it shows a

    custom offline message to a user when they are offline.
  15. <!doctype html> <html> <head> </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> index.html
  16. event.respondWith( fetch(event.request).catch(error => { // Do something about it })

    ); service-worker.js (detail) handle the offline situation in the catch
  17. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(

    fetch(request).catch(error => { return new Response('<p>Oh, dear.</p>', { headers: { 'Content-Type': 'text/html' } }); }) ); } }); service-worker.js
  18. index.html service-worker.js self.addEventListener('fetch', event => { if (event.request.mode === 'navigate')

    { event.respondWith( fetch(request).catch(error => { return new Response('<p>Oh, dear.</p>', { headers: { 'Content-Type': 'text/html' } }); }) ); } }); <!doctype html> <html> <head> </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>
  19. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(

    fetch(request).catch(error => { return new Response('<p>Oh, dear.</p>', { headers: { 'Content-Type': 'text/html' } }); }) ); } }); service-worker.js Let’s not do this…
  20. <!doctype html> <html> <head> <meta charset="utf-8"> <title>Registering a Service Worker</title>

    <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="/default.css" rel="stylesheet" title="Default Style"> </head> <body> <h1>Offline Fallback Test</h1> <p class="error">If you see this, you are offline</p> </body> </html> offline.html instead, let’s use a real HTML page
  21. We’ll extend our Service Worker so that it provides a

    custom offline page when navigation requests are made and the network is unavailable.
  22. <!doctype html> <html> <head> <meta charset="utf-8"> <title>Registering a Service Worker</title>

    <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="/default.css" rel="stylesheet" title="Default Style"> </head> <body> <h1>Offline Fallback Test</h1> <p class="error">If you see this, you are offline</p> </body> </html> offline.html Gotta stash the offline page first!
  23. self.addEventListener('install', event => { var offlineURL = 'offline.html'; event.waitUntil( );

    }); service-worker.js (install event handler detail) service-worker.js (install event handler detail)
  24. self.addEventListener('install', event => { var offlineURL = 'offline.html'; event.waitUntil( fetch(new

    Request(offlineURL)).then(response => { }) ); }); Step 1 service-worker.js (install event handler detail)
  25. self.addEventListener('install', event => { var offlineURL = 'offline.html'; event.waitUntil( fetch(new

    Request(offlineURL)).then(response => { return caches.open('offline').then(cache => { }); }) ); }); Step 2 service-worker.js (install event handler detail)
  26. self.addEventListener('install', event => { var offlineURL = 'offline.html'; event.waitUntil( fetch(new

    Request(offlineURL)).then(response => { return caches.open('offline').then(cache => { return cache.put(new Request(offlineURL), response); }); }) ); }); Step 3 service-worker.js (install event handler detail)
  27. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(

    fetch(request).catch(error => { return new Response('<p>Oh, dear.</p>', { headers: { 'Content-Type': 'text/html' } }); }) ); } }); service-worker.js (fetch event handler detail) instead of…
  28. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(

    fetch(event.request).catch(error => { return caches.open('offline').then(cache => { }); }) ); } }); service-worker.js (fetch event handler detail)
  29. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(

    fetch(event.request).catch(error => { return caches.open('offline').then(cache => { return cache.match('offline.html'); }); }) ); } }); service-worker.js (fetch event handler detail) You can use URL in place of Request here…
  30. We can make a Service Worker that implements different network

    strategies for different types of requests.
  31. 1 2 Determining what kind of request a given fetch

    represents. Getting stuff into caches so it’s there for later.
  32. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(/*

    network-first strategy */); } }); 1 service-worker.js (fetch handler detail)
  33. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(/*

    network-first strategy */); } else if (event.request.headers.get('Accept').indexOf('image') !== -1) { event.respondWith(/* cache-first strategy */); } }); 1 service-worker.js (fetch handler detail)
  34. 1 2 Determining what kind of request a given fetch

    represents. Getting stuff into caches so it’s there for later.
  35. Response.ok if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => {

    if (response.ok) { } }) ); } service-worker.js (fetch handler detail) 2
  36. Response.clone() if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => {

    if (response.ok) { caches.open('assets').then(cache => { cache.put(event.request, response.clone()); }); } }) ); } service-worker.js (fetch handler detail) 2
  37. 2 if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => {

    if (response.ok) { caches.open('assets').then(cache => { cache.put(event.request, response.clone()); }); } return response; }) ); } service-worker.js (fetch handler detail)
  38. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { if

    (response.ok) { caches.open('assets').then(cache => { cache.put(event.request, response.clone()); }); } return response; }) ); } service-worker.js (fetch handler detail)
  39. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    ); } service-worker.js (fetch handler detail) fold happy path out of the way…
  40. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { return caches.match(event.request).then(response => { }); }) ); } service-worker.js (fetch handler detail)
  41. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { return caches.match(event.request).then(response => { if (!response) { throw Error(`${request.url} not found in cache`); } }); }) ); } service-worker.js (fetch handler detail)
  42. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { return caches.match(event.request).then(response => { if (!response) { throw Error(`${request.url} not found in cache`); } return response; }); }) ); } service-worker.js (fetch handler detail)
  43. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { return caches.match(event.request).then(response => { if (!response) { throw Error(`${request.url} not found in cache`); } return response; }); }) ); } service-worker.js (fetch handler detail) service-worker.js (fetch handler detail)
  44. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { }) ); } service-worker.js (fetch handler detail) service-worker.js (fetch handler detail)
  45. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { }) .catch(() => { }) ); } service-worker.js (fetch handler detail) service-worker.js (fetch handler detail)
  46. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { }) .catch(() => { return caches.open('offline').then(cache => { return cache.match('offline.html'); }); }) ); } service-worker.js (fetch handler detail) service-worker.js (fetch handler detail)
  47. 1 2 Put assets into cache during install phase. Later,

    respond to fetches for static assets with a cache-first strategy.
  48. 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' ]; service-worker.js (detail) 1
  49. self.addEventListener('fetch', event => { var url = new URL(event.request.url); if

    (cacheFiles.indexOf(url.pathname) !== -1) { event.respondWith( ); } }); 2 service-worker.js (fetch handler detail) responding to fetch events for app shell assets
  50. self.addEventListener('fetch', event => { var url = new URL(event.request.url); if

    (cacheFiles.indexOf(url.pathname) !== -1) { event.respondWith( caches.match(event.request).then(response => { if (!response) { throw Error(`${event.request.url} not found in cache`); } return response; }) ); } }); 2 service-worker.js (fetch handler detail)
  51. self.addEventListener('fetch', event => { var url = new URL(event.request.url); if

    (cacheFiles.indexOf(url.pathname) !== -1) { event.respondWith( caches.match(event.request).then(response => { if (!response) { throw Error(`${event.request.url} not found in cache`); } return response; }).catch(() => fetch(event.request)) ); } }); 2 service-worker.js (fetch handler detail)
  52. self.addEventListener('install', event => { event.waitUntil( caches.open('static').then(cache => { return cache.addAll(cacheFiles);

    }).then(() => self.skipWaiting()) ); }); service-worker.js (install handler detail) Make SW activate immediately
  53. self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheKeys => { var oldCacheKeys

    = cacheKeys.filter(key => { return (key.indexOf(cachePrefix) !== 0); }); }) ); }); service-worker.js (activate handler detail) Array.prototype.filter
  54. 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); }); }) ); }); Array.prototype.map service-worker.js (activate handler detail)
  55. 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 detail)
  56. 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()) ); }); Make SW take control immediately
  57. • Recipes for: • creating an offline image • Using

    an external JSON file for managing application shell asset URLs • Links to resources and documentation Repository also includes:
  58. Pragmatist’s Guide to Service Worker I am Lyza Danger Gardner

    I am an Open Web Engineer at Bocoup I can be found at @lyzadanger and https://www.lyza.com Smashing Conference NYC 2017 http://bit.ly/sw-smashing-nyc