Skip to content

Instantly share code, notes, and snippets.

@oilsinwater
Last active June 27, 2022 03:36
Show Gist options
  • Save oilsinwater/2c3a8d2011bebbf32bd5d6e6981e967c to your computer and use it in GitHub Desktop.
Save oilsinwater/2c3a8d2011bebbf32bd5d6e6981e967c to your computer and use it in GitHub Desktop.
Service Workers

Service Workers

@(js study notes)[code, js, dev]

Can be used to intercept requests sent from the browser; for this reason only works over TLS so the server must have SSL cert. This is useful for offline first approach (progressive web applications) because 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 workers will not reload until the page is navigated away from. Refreshing does not spawn a new service worker because the original 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 refresh. This feature is under "Applications" tab. Alternatively Shift + F5 (reload) can be used to reload the Service Worker.

Service Worker Lifecycle

Registration

Tells the browser where your service worker is located, and to start installing it in the background.

javascript
// in an entry file e.g. 'main.js' looks for browser support as a property in the navigator object
if (!('serviceWorker' in navigator)) {
  console.log('service worker not supported');
  return;
  }
  navigator.serviceWorker.register(
    '/service-worker.js'
    )
    .then(function(registration) {
      console.log('service worker registered! Scope is:',
      registration.scope);
      });
      // .catch a registration error

Installation

javascript
// once registered; separate file 'service-worker.js'
self.addEventListener('install', function(event) {
// do stuff during install
  });

Activation

javascript
self.addEventListener('activate', function(event) {
  // Do stuff during install
  });
  

How to register a service worker

  • navigator.serviceWorker.register() is the method used to register a service worker. It takes two parameters:
  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 worker. 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:

Below is a table of service worker events.

Events Function Events
Install Fetch
Activate sync
Message push

Intercept fetch

The following service worker will intercept all fetch requests and then send that request out again using fetch. If thefetch 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.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');
      }t
      // This line returns the response to the respondWith() method. It will either be the original response or the dr-evil gif.
      return response;
    })
    // The .catch will catch all promse errors. Should dr-evil not work or should some other error occur.
    .catch(function() {
      return new Response("Uh oh, that totally failed!");
    })
  );
});
  • self.addEventListner(): self refers to the current service worker window. Service workers do not have a window object unlike normal windows do, so this is why self keyword is used.

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(), it 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 sophisticated 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);
      })
    );
    })
  );
});
  • Tip: 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 Registration 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.unregister()

  • reg.update()

Properties

reg.installing - 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 happened to the service worker.

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

});

Service Worker States:

The service worker has a .state property 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.

Service 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.controller 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 notification 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 waiting 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()

  • skipWaiting() is a method on a servicer worker that will tell it to stop waiting and immediately take control of the page.

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

  • postMessage() is a 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 'controllerchange'event that is thrown as the service worker controlling the page has changes. This event is the perfect opportunity to do a reload of a page, this way the new service worker along with all its changes and cache will be presented to the user.

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

Caching 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 served 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 cache 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