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

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