Skip to content

Instantly share code, notes, and snippets.

@gregpawlowski
Last active April 8, 2018 16:52
Show Gist options
  • Save gregpawlowski/0bf86cb40199b98b5d94ab3ac51a92d3 to your computer and use it in GitHub Desktop.
Save gregpawlowski/0bf86cb40199b98b5d94ab3ac51a92d3 to your computer and use it in GitHub Desktop.
Google Mobile Web: Introducing The Service Worker

Service Workers

Can be used to intercept requests sent from the browser. this is useful for offline first approach becuase API requests (or other requests) can be intercepted and be served out from a cache instead.

  • Service workers spawn a new window on registration.
  • Service woerkers will not reload until the page is navigated away from. Refreshing does not spawn a new service worker because the orignal one is still active until the page is navigated away from.
  • Google chrome has a useful feature in Dev Tools for reloading service workers on referesh. This feature is under "Applications" tab. Althernatively Shift + F5 (reload) can be used to reload the Service Worker.

How to register a service worker

navigator.serviceWorker.register() is the method used to register a service worker. It takes two paramters:

  1. URL of service worker (string)
  2. Options object (object)

The options object so far has one option: scope. { scope: "/posts/" }

scope should be a string that will point to the routes that you want the service worker to look at. If you use the the scope of "/posts/" then only www.something.com/posts and all child directories will be controlled by the service woerkre. The main URL www.something.com will not.

// Check if service worker is available (for older browser support)
if (!navigator.serviceWorker) return; 

// Register service worker
// '/sw.js' is the path to the service worker
navigator.serviceWorker.register('/sw.js').then(function() {
  console.log('Registration worked!');
}).catch(function() {
  console.log('Registration failed!');
});

Service Worker:

Service workers

Intercept fetch

The following service worker will intercept all fetch requests and then send that request out again using fetch. If the fetch returns a 404 (not found status) then the service worker will fetch a gif and that will be served up instead.

// Add event listener for the "fetch" event. This is added to self. Self refers to the current service worker window. Service Werkers do not have a window object like normal windows so this is why the self keyword is used.
self.addEventListener('fetch', function(event) {
  event.respondWith(
    // Send out the intercepted request using fetch
    fetch(event.request).then(function(response) {
      // If the request URL is not found (404) then fetch Dr Evil gif.
      if (response.status === 404) {
        // Since we are doing a fetch inside a fetch (promise within a promise) we can just return the promise.
        return fetch('/imgs/dr-evil.gif');
      }
      // This line returns the response to the respondWith() method. It will either be the original reponse or the dr-evil gif.
      return response;
    })
    // The .catch will catch all promse errors. Should dr-evil not work or should some other error occcur.
    .catch(function() {
      return new Response("Uh oh, that totally failed!");
    })
  );
});

Caches API

Cache API can be used to cache requests and responses into a browser cache. The cache box on the browser contains request/response pairs from any origin. The cache API provides a caches object on the windows object to work with the cache.


Open a cache

To open a cache use cache.open()

The open method will return a cache object. If that cache does not exit, it will create one. You can then work with the object using methods like:

caches.open('my-stuff').then(function(cache) {

})

Cache Methods

cache.put()

Add cache item pair to the cache

cache.put(req, res);

cache.addAll()

Takes array of requests urls, fetches them and adds them to the cache:

If any of these URLs fail to cache, none of them are added!

cache.addAll([
  'foo',
  '/bar'
])

cache.match()

Takes a request or a URL and returns a promise for a matching response if one is found or null.

cache.match(request);

caches.match()

Same as cache.match except tries to find match in any cache, starting with the oldest.

caches.delete()

Take a cache name (string). Returns a promise.

caches.keys()

Returns a list of cached entries. Returns a promise.

Service Workers Again...

Install Event

When a browser runs a service worker for the first time, an install event is fired within it. The install event is the perfect place to save things to the cache.

The following code will run when the install event is fired. It will wait until all of the the pages (main page "/" and "main.js") get added to the cache.

self.addEventListener('install', function(event) {
  // Wait until all promises are resolved. The service worker won't be installed until then.
  event.waitUntil(
    // Open the cache with the anme 'wittr-static-v1' if it doesn't exist it will be created.
    caches.open('wittr-static-v1').then(function(cache) {
      // Add the following URLs to the cache. Each URL will be fetched and added to cache.
      return cache.addAll([
        '/',
        'js/main.js'
      ]);
    })
  );
});

Fetch from Cache

This Service Worker code will intercept all fetch requests. It will then respond with the cached response if one is found, if not it will fetch the data using the fetch API.

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // Serach all chache for a request URL thta matches the one served up from the fetch.
    caches.match(event.request)
    .then(function(response) {
      // If a response exists return that, otherwise return fetch(event.request). Since fetch returns a promise, it's ok to return that because it's a promise within a promise. The || operator is the OR operator. If the first statemnt is true it stops running, if it's not it goes to check the next statement.
      return response || fetch(event.request);
      })
  );
});

Updating Static Cache (Service Worker Lifecycle)

Since our cache is updated with each service worker install event step it will not get updated unless a new service worker is spawned up and activated, and since service workers do not get activated until a page is navigated away from this poses a problem as our website will continuassly load from the same old cache that was created when the service worker was installed.

A way to work around this problem is to do the following:

  1. Updated the service worker to point to a new cache
    • this will cause a new service worker to spawn up but not install yet.
    • The new service worker will install and fetch the content and push it oto the new cache.
  2. Once the new servcie worker is ready we can delete the old cache and release the old service worker.

Activate Event

The activate event will fire when the install event is complete and the service worker is activating at this point but it's not controlling any pages.

The active event is the perfect event to clear up old cache because at this point the service worker was already installed so the new cache should have been created and there is no need for the old cache.

self.addEventListener('activate', function(event) {
  // Clear up old cache or do other things during the activate event.
  // Becuase this process may take some time, make sure to use event.WaitUntil() this will prevent new event going to the servcie worker until this process is completed.  
});

More sephisticated approach:

let staticCacheName = "wittr-static-v3";

self.addEventListener('activate', function(event) {
  event.waitUntil(
    // Get all keys from the cache
    caches.keys().then(function(cacheNames) {
    // Because the map function below will return an array of promises from cache.delete we wrap the code in a Promise.all to wait until all promises are compelted / all cache instances are removed.
    return Promise.all(
      // Filter out caache names not related to our app
      cacheNames.filter(function(cacheName) {
        // The filter callaback function will only return cache names that stat with wittr- and are not the same as our static cache name defined above
        return cacheName.startsWith('wittr-') && cacheName !== staticCacheName;
      })
      // the map function is called because filter will return an updated array. Map will then modify that array. It will fun a for loop for each time and run the delete. The delete will return a promise wich will replace each item in the array. At the end we will have an array of promises. This is why Promise.all is used above to wait until the array of all promises is complete.
      .map(function(cacheName) {
        return caches.delete(cacheName);
      })
    );
    })
  );
});

It is also very good cache practice to add versions into your content. This can be updated at build (using gulp or some other build system) and will avoid your browser fetching content from it's http cache. It's also a good idea to have the new cache name be auto-generated.

Service Worker Regisation Object

When you register a service worker that service worker returns a Promise. That Promise returns/fulfills with a service work registration object. It has properties and methods related to the service workers.

Methods

reg.unregiter()

reg.update()

Properties

reg.intalling - Points to a Service worker that is in installing state or is null.

reg.waiting - Points to the waiting service worker (the one ready to take over control of the page)

reg.active - Points to the currently active service worker.

Events

Registration worker will emit an event when an update has happend to the servie worker.

reg.addEventListener('updatefound', function() {

});

Service Worker States:

The service worker has a .state proprty that can be checked to see what status the service worker is currently.

installing - Service worker is installing but not yet installed.

installed - Service worker is installed but not yet activated.

activating - Service worker is activating but not done yet.

activated - Service worker is activated and ready to be used.

redundant - Service worker has been thrown away. If service worker has been replaced by another or failed to install.

Servic Worker Events:

Service worker throw a statechange event when the state of the service worker changes.

sw.addEventListener('statechange', function() {
  // state has changed. 
  // you can check the sw.state
});

Navigator.serviceWorker.controller

navigator.serviceWorker.conroller will point to the current service worker that is controlling the page.

Listening for an update to a service worker

This code registers a service worker, then adds checks on registration to check if any updated service worker come online.

// Note: The _updateReady() function makes the notifictoin appear on the page.

IndexController.prototype._registerServiceWorker = function() {
  if (!navigator.serviceWorker) return;

  var indexController = this;

  navigator.serviceWorker.register('/sw.js').then(function(reg) {
    if (!navigator.serviceWorker.controller) return;

    // If reg.waiting is not null that means that there is a service worker wiating to take over control. In that case 
    if (reg.waiting) {
      indexController._updateReady();
      return;
    }

    // If reg.installing is not null that means there is a current  
    if (reg.installing) {
      indexController._trackInstalling(reg.installing);
      return;
    }

    // Since the above conditions will only check once and only when the service worker is first registered we have to add a updatefound event handler. This handler will will be called when an updated service worker is found.
    reg.addEventListener('updatefound', function() {
      // When the update found event is triggered then we know that a service worker will be in installing state so we call  the Track installing function on the installing service worker. 
      indexController._trackInstalling(reg.installing);
    });
    
  });
};

IndexController.prototype._trackInstalling = function(worker) {
  // We have to create a pointer for "this". Alternatively we can use arrow functoins in the addEventListener below.  
  var indexController = this;
  // Check for state changes to the current worker.
  worker.addEventListener('statechange', function() {
    // If the worker state is installed then send update to the user.
    if (worker.state === 'installed') {
      indexController._updateReady();
      // If we used arrow function instead then we can use this:
      // this._updateReady();
    }
  });
};

skipWaiting() and postMessage()

Skip waitng is a method on a servicer worker that will tell it to stop wiating and immidietly take control of the page.

self.skipWaiting() - Take over control of the page right away. This is a method on the service worker itself.

postMessage() - Method used to send a message to the service worker. This method lives on the service worker object of the page that is being controlled by a service worker.

//from page:
reg.installing.postMessage({foo: 'bar'});

//on service worker:
self.addEventListner('message', function() {
  event.data; // {foo: 'bar'}
});

controllerchange event

The Service Worker has a contollerchange event that is thrown is the service worker controlling the apge has changes. This event is the perfect opertunity to do a reload of a page, this way the new service worker along iwth all its changes and cache will be presented to the user.

navigator.serviceWorker.controller.addEventListener('controlllerchange', function() {
  // reload the page becuase a new service worker has taken over.
});

Cacheing a skeleton page

Since this application is a single page application, it makes no sense to cache all the old post content. Instead we would cache just the skeleton page (which is erveed up by the server under "/skeleton") and the application will make fetch request for the content. It seems like next we'll learn of a way to chache the posts using indexedDB.

// In the install event we need to fethc the skeleton page wich is served up by the server in /skeleton/
self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(staticCacheName).then(function(cache) {
      return cache.addAll([
        '/skeleton'
    }
}

// In the fetch event handler.
self.addEventListener('fetch', function(event) {
  // convert the fetch rquest URL to a URL object.
  let reqURL = new URL (event.request.url);
  // Now that the reqURL is a URL object we can check the origin property to make sure the origin of this url matches the origin of the service worker.
  if (reqURL.origin === location.origin) {
    // Check to make usre the URL reqeust for the for the root page ("/")
    if (reqURL.pathname === "/") {
      // If it is, then respond with the skeleton page
      event.respondWith(caches.match('/skeleton'));
      return;
    }
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment