Wiljan Slofstra

Cache met Service Workers

Ondersteuning voor Service Workers beginnen door te dringen in moderne browsers. Met Service Workers kunnen we nieuwe functionaliteiten toevoegen aan web applicaties die voorheen onmogelijk waren — cache, push notificaties en offline pagina’s. In de toekomst zullen we hier nog meer uitbreidingen op zien die dezelfde API in gebruik zullen nemen.

Een Service Worker is een script dat in de achtergrond draait van de browser. Het heeft ook geen connectie met de DOM. Zodra de Service Worker geïnstalleerd is kan het daarom los van de web applicatie draaien. Dit maakt het mogelijk om push notificaties naar de gebruiker te sturen als de website niet open staat.

Een bijkomend voordeel is dat een Service Worker gebruikt kan worden als progressive enhancement. Dat betekent dat het geen probleem is als de browser het niet ondersteund, dan wordt het gewoon niet gebruikt. Als het wel ondersteund wordt is het een mooie toevoeging en een betere ervaring voor de gebruiker. We kunnen Service Workers nu al inzetten.

Regels

Er zijn wel een aantal regels waaraan de applicatie moet voldoen om een Service Worker te kunnen gebruiken.

Ten eerste moet de applicatie met https beveiligd zijn. Om ontwikkeling makkelijker te maken kan het ook gebruikt worden met localhost. Service Workers zijn krachtig omdat het een proxy is voor elke netwerk request die de website uitvoert. Dat betekent ook dat alles wat gebruikt wordt in een applicatie gewijzigd kan worden door een Service Worker. Om veiligheid te waarborgen en man-in-the-middle attacks te voorkomen is deze restrictie opgelegd.

Ten tweede is de Service Worker alleen beschikbaar op het pad waar de Service Worker staat. Dat komt erop neer dat wanneer de Service Worker geïnstalleerd is op www.example.com/mijn-app/ deze niet toegankelijk in www.example.com/index.html maar wel in www.example.com/mijn-app/index.html. De Service Worker moet dus altijd in de root van de applicatie geplaatst worden.

Als laatste moet er opgelet worden welke functie binnen Service Workers werken en welke niet. Zo mist Chrome enkele cache functies. Daarvoor zijn wel polyfills die geïmporteerd kunnen worden en die we zo in werking zien. Voor een overzicht van ondersteuning is er de website isserviceworkerready van Jake Archibald.

Installatie

Een Service Worker werkt pas bij het tweede bezoek. De Service Worker wordt namelijk pas geïnstalleerd als de pagina klaar is met laden. We kunnen daarom het initialisatie script toevoegen aan de bestaande javascript.

// Test Service Worker support
if ('serviceWorker' in navigator) {
	// Registreer de sw.js Service Worker
	navigator.serviceWorker.register(‘sw.js’);
}

De basis voor een Service Worker ziet er als volgt uit:

// sw_base.js
/* global importScripts */

const version = 1;

// Importeer cache polyfill voor Chrome
importScripts('serviceworker-cache-polyfill.js');

// Event die wordt aangeroepen bij installatie van de Service Worker, hier kunnen we bestanden gaan cachen en push notificaties aanzetten
self.addEventListener('install', function installServiceWorker(event) {
  console.log('[ServiceWorker]: Install version: ' + version);
});

// Fetch event wordt aangeroepen bij iedere netwerk aanvraag
self.addEventListener('fetch', function fetchEvent(event) {
  console.log('[ServiceWorker]: Fetch: ' + event.request.url);
});

// Activate wordt aangeroepen na de installatie, hier kunnen bijvoorbeeld oude cache bestanden worden verwijderd
self.addEventListener('activate', function activateEvent(event) {
  console.log('[ServiceWorker] Activate');
});

Met deze regels hebben we nog niks, maar het is de basis van elke Service Worker.

Debuggen

Het debuggen van Service Workers is nog ver van ideaal. Chrome heeft een pagina waar alle Service Workers staan — chrome://serviceworker-internals/. Hier kunnen Service Workers geïnspecteerd, gestopt en verwijderd worden.

Als we bovenstaande code inspecteren komt er ongeveer het volgende te staan:

Service Workers in console output

Cache

Nu naar een echt voorbeeld wat ook toe te passen is. Het expliciet cachen van bestanden. Ten eerste de installatie stap:

// Bestanden die gecached moeten worden
const cacheItems = [
  'http://unsplash.it/640/480/',
  'http://unsplash.it/641/480/',
  'http://unsplash.it/642/480/',
	'https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css',
];

self.addEventListener('install', function installServiceWorker(event) {
  console.log('[ServiceWorker]: Install version: ' + version);

  // Wacht totdat de Service Worker is geïnstalleerd
  event.waitUntil(
    // Open de cache voor de huidige versie
    caches.open(version).then(function openCachePromise(cache) {
      console.log('[ServiceWorker] Cached files', cacheItems, version);

      // Voeg alle bestanden
      return cache.addAll(cacheItems);
    }).then(function forceActivate() {
      // Forceer activatie
      return self.skipWaiting();
    })
  );
});

We hebben nu de vier bestanden in de cacheItems array toegevoegd aan de cache. Nu moeten we de bestanden nog uit de cache teruggeven als ze worden opgevraagd, dit doen we in de fetch stap.

self.addEventListener('fetch', function fetchEvent(event) {
  // Stuur een nieuwe response naar de gebruiker
  event.respondWith(
    // Kijk of de request in de cache zit
    caches.match(event.request)
      // Een promise die de response teruggeeft als het gevonden is in de cache of null als het er niet is
      .then(function matchedRequest(response) {
        // Als er een response is geven we deze terug aan de gebruiker
        if (response) {
          console.log('[Fetch] Returning from Service Worker cache: ', event.request.url);
          return response;
        }

        console.log('[Fetch] Returning from server: ', event.request.url);

        // De request is niet in de cache, met fetch kunnen we het online ophalen
        return fetch(event.request);
      }
    )
  );
});

We hebben nu een werkende Service Worker die bestanden cached. Er is nog wel een stap die het allemaal wat netter maakt en dat is de activatie stap. Hier gaan we de oude cache legen — dat is de cache die niet het huidige versie nummer heeft.

self.addEventListener('activate', function activateEvent(event) {
  // De versies die niet geleegd hoeven worden, we voegen nu alleen onze huidige cache versie toe
  const cacheWhitelist = [version];

  console.log('[ServiceWorker] Activate');

  // Wacht op activatie
  event.waitUntil(
    // Haal alle cache versies op (bijv. [‘v1’, ‘v2’])
    caches.keys().then(function keyListPromise(keyList) {
      // Ga over elke versie heen
      return Promise.all(keyList.map(function keyListIteration(key, i) {
        // Controleer of deze versie in de whitelist zit, zo niet verwijder het bestand
        if (cacheWhitelist.indexOf(key) === -1) {
          return caches.delete(keyList[i]);
        }
      }));
    })
  );
});

Bekijk het volledige bestand op Github.

Tools

Om het leven een stuk makkelijker te maken heeft Google een aantal tools gemaakt die bovenstaande code een stuk simpeler maakt.

SW-Toolbox

De Service Workers toolbox is een bibliotheek met helper functies. Bovenstaande cache systemen staan daar al standaard in en hoeven alleen maar aangeroepen te worden. De toolbox kan gebruikt worden door het script te importeren:

importScripts('sw-toolbox.js');

Bestanden kunnen gecached worden door:

toolbox.precache(['/site.css', '/images/logo.png']);

De SW-Toolbox maakt gebruikt van een router om het makkelijker te maken wat gecached moet worden. Een voorbeeld van de SW-Toolbox Github pagina om YouTube thumbnails op te slaan:

// Maak een route aan
// - '/(.*)' Match alle urls
// - toolbox.cacheFirst Gebruik een standaard strategie voor caching
global.toolbox.router.get('/(.*)', global.toolbox.cacheFirst, {
	// Maak een nieuwe cache aan voor deze bestanden
	cache: {
	  name: 'youtube-thumbnails',
	  // Maximaal 10 items
	  maxEntries: 10,
	  // Cache van bestanden zijn maximaal 30 seconden beschikbaar
	  maxAgeSeconds: 30
	},
	// Cache alleen bestanden van de volgende origin
	origin: /\.ytimg\.com$/
});

// Standaard route voor bestanden die door de bovenstaande route(s) niet zijn afgevangen
global.toolbox.router.default = global.toolbox.networkFirst;

In SW-toolbox zitten standaard verschillende strategieën voor het cachen van bestanden. In het voorbeeld hierboven gebruiken we er twee ‘cacheFirst’ en networkFirst. Een paar mogelijke strategieën:

Natuurlijk is het mogelijk om deze zelf te bouwen, maar bovenstaande strategieën maken het een stuk makkelijker.

Voor de volledige documentatie: https://github.com/GoogleChrome/sw-toolbox

SW-precache

Dit is een script dat gebruikt kan worden in build scripts zoals Gulp en Grunt. Het genereert een Service Worker script om bestanden te cachen. Door het gebruik in een build process is het mogelijk om nieuwe bestanden automatisch toe te voegen en om de cache versie automatisch te laten gaan. Op deze manier is geen omkijken meer naar. Voor Gulp ziet het gebruik er als volgt uit (voorbeeld van de SW-precache Github):

gulp.task('generate-service-worker', function(callback) {
  var path = require('path');
  var swPrecache = require('sw-precache');
  var rootDir = 'app';

  swPrecache.write(path.join(rootDir, 'service-worker.js'), {
    staticFileGlobs: [rootDir + '/**/*.{js,html,css,png,jpg,gif}'],
    stripPrefix: rootDir
  }, callback);
});

Voor de volledige documentatie kijk op: https://github.com/GoogleChrome/sw-precache

Conclusie

Service Workers zijn een mooie toevoeging op het web platform. Het biedt geweldige nieuwe mogelijkheden, zoals cache, push notificaties en offline pagina’s. In dit artikel heb ik alleen de eerste besproken, maar ik ben van plan de andere twee ook te gaan beschrijven.

Dit is nog maar het begin van de Service Workers, het is nog relatief nieuwe technologie wat nu langzamerhand mainstream wordt. Chrome heeft een vrij goede ondersteuning en Firefox krijgt het in de volgende versie. Service Workers zijn echter gemaakt om nieuwe uitbreidingen mogelijk te maken, zo komen er bijvoorbeeld nog de Geofencing API en Background sync API.

Omdat het bij Service Workers om progressive enhancement gaat kunnen we ze gebruiken.

Terug naar boven