Service Workers

A Quick Introduction

http://briebug.github.io/presentations/ng-meetup/2017/11/service-workers
Kevin Schuchard
kevin.schuchard@briebug.com

Service Workers

A progressive upgrade for modern web apps

Safe Man in the Middle for network

Offline First!

Treat network as a nice-to-have upgrade

Build for offline first, right next to mobile first

Secure

They only work over HTTPS connections

Can only be developed at localhost

Context

Determined by script path (default)

Can be overridden

What can they do now?

Caching

Preloading

Web Push Notifications

Background Sync

What will they do in the future?

Periodic Sync

LBS/Geofencing

Growing browser support

Chrome, Firefox, Opera & Edge

Safari support coming!

Is ServiceWorker Ready?

Service Worker Lifecycle

  • Installation
  • Activation
  • Idle/Message Loop
    • Fetch/Message
    • Terminate

Creating a ServiceWorker

Reliably register

Install & Activate

Handle network requests, cache data, etc.

Registration

Guard with 'serviceWorker' in navigator

Is idempotent


if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker
      .register('/myServiceWorker.js')
      .then(function(reg) {
        console.log('Registration successful: ', reg.scope);
      }, function(err) {
        console.log('Registration failed: ', err);
      });
  });
}
          

Installation

Resides within service worker script

First opportunity to cache data

Can preload data into cache here


self.addEventListener('install', function(event) {
  // Add installation steps/code here
});

          

Activation

Runs after old workers have been stopped

Resides within service worker script

Opportunity to remove old/unused caches and data


self.addEventListener('activate', function(event) {
  // Add installation steps/code here
});
          

Fetch

Primary event handler in service workers

Opportunity to intercept network requests and cache data


self.addEventListener('fetch', function(event) {
  return fetch(event.request); // Don't cache, just pass it through (for now)
});
          

Preload & Cache

  • During Install:
    • Preload & cache essential files
    • Site content, data, anything you need ahead of time
  • During fetch:
    • Return data from cache if we have it
    • Otherwise:
      • If online, fetch from network
      • If offline, fall back to preloaded offline page (etc.)

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches
      .open('siteFiles')
      .then(function(cache) {
        // Preload the cache with core files for site
        return cache.addAll([
          '/assets/fonts/font.woff2',
          '/assets/fonts/font.otf',
          '/assets/fonts/font.ttf',
          '/assets/css/main.css',
          '/assets/offline.png',
          '/offline.html'
        ]);
      });
  );
});
          

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches
      .match(event.request) // Look for request in ALL caches
      .then(function(res) {
        if (res) { // We've got it in the cache, return cached copy
          return res;
        }

        if (!navigator.onLine) { // Definite if offline!!
          // Return the offline page, already cached in preload during install
          return caches.match(new Request('/offline.html'));
        }

        return fetch(event.request);
      });
  );
});
          

Clean up Old Caches

During activaton, opportunity to remove unused cached data


self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches
      .keys().then(function(keys) {
        return Promise.all(keys.filter(function(key) {
          return ['oldDataCache','otherUnusedCache'].indexOf(key) !== -1;
        }).map(function(key) {
          return caches.delete(key); // Remove old caches of data
        }));
      });
  );
});
          

Keep Cache Updated

During fetch, cache data retrieved from network


const siteFiles = [
  '/assets/fonts/font.woff2',
  '/assets/fonts/font.otf',
  '/assets/fonts/font.ttf',
  '/assets/css/main.css',
  '/assets/offline.png',
  '/offline.html'
];

function fetchAndCache(request) {
  return fetch(request)
    .then(function(res) {
      if (res) {
        let cacheName = siteFiles.index(res.url) === -1 ? 'data' : 'siteFiles';
        return caches
          .open(cacheName)
          .then(function(cache) {
            return cache.put(request, res.clone());
          });
      }
    });
}
          

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches
      .match(event.request) // Look for request in ALL caches
      .then(function(res) {
        if (res) { // We've got it in the cache, return cached copy
          return res;
        }

        if (!navigator.onLine) { // Definite if offline!!
          // Return the offline page, already cached in preload during install
          return caches.match(new Request('/offline.html'));
        }

        return fetchAndCache(event.request); // Fetch and update the cache!
      });
  );
});
          

ServiceWorker API Family

ServiceWorker

CacheStorage

Cache

Fetch

Web Workers

Cache API

Designed to work hand in hand with service workers

Supports multiple stores of cached of data

Living standard

Is promisy!

Fetch API

Modern replacement for XHR

Simple, easy to use

Living standard

Is promisy!

Web Workers API

Parent of ServiceWorkers

Underlying background workers API

Living standard

Sw-Precache

Generates a service worker for you

Pre-caches site content files

sw-precache --root=dist --config=sw-precache-config.js


// sw-precache-config.js
module.exports = {
  navigateFallback: '/index.html',
  stripPrefix: 'dist',
  root: 'dist/',
  staticFileGlobs: [
    'dist/index.html',
    'dist/**.js',
    'dist/**.css',
    'dist/fonts/**.woff',
    'dist/fonts/**.otf',
    'dist/fonts/**.ttf'
  ]
};
          

SW-Toolbox

Express-style routes and request handling

Adds policies like middleware

Network first, Cache first, etc.

Caching configuration (expiration, maxEntries, etc.)

Works with sw-precache


importScripts('/sw-toolbox.js');

// Toolbox way of wiring up the offline page:
toolbox.router.get('/*', function(request, values, options) {
  return toolbox.networkFirst(request, values, options)
    .catch(function(err) {
      return caches.match(new Request('/offline.html'));
    });
}, {
  networkTimeoutSeconds: 5,
  cache: {
    name: 'staticFiles',
    maxAgeSeconds: 60 * 60 * 24 * 30 // one month
  }
});
          

// Retrieve data as quickly as possible:
toolbox.router.get('/api/comments*', function(request, values, options) {
  return toolbox.fastest(request, values, options); // Generally use cache, keeps cache fresh
}, {
  networkTimeoutSeconds: 5,
  cache: {
    name: 'comments',
    maxAgeSeconds: 60 * 60 * 15 // Keep data for 15 minutes
  }
});
          

References