Skip to main content
Dublin Library

The Publishing Project

Service Workers: plain and workbox styles: Plain

 

The idea is to create the service worker using only native APIs and seeing how much of the Workbox functionality I can duplicate without having to use a library.

Constants and common items #

Individual constants for each of the caches. We store them separately so we can change the names to make cleanup easier without having to change the array we define later.

const PRECACHE = 'precache-v1';
const CONTENT = 'content-cache-v1';
const CSS = 'css-cache-v1';
const JS = 'scripts-cache-v1';
const IMAGES = 'image-cache-v1';
const FONTS = 'fonts-cache-v1';

expectedCaches is the array of cache names we expect to exists in the client.

If they don't we're fine, we fetch from the network; If they do, we're fine, we fetch from the cache and if they exist with a different version then we delete the old one

const expectedCaches = [
  PRECACHE,
  CONTENT,
  CSS,
  JS,
  IMAGES,
  FONTS,
];

urlsToPrecache is the list of URLs that we want to cache on install. The / URL indicates the root file, in this case, index.html

const urlsToPrecache = [
  '/index.html',
  '/css/index.css',
  '/js/zenscroll.js',
  '/404.html',
  '/offline.html',
];

Install Event #

The install event will take all the URLs in the urlsToPrecache array

self.addEventListener('install', (event) => {
  self.skipWaiting();
  console.log('install event fired');
  event.waitUntil(caches.open(PRECACHE).then((precache) => {
    return precache.addAll(urlsToPrecache);
  })); // ends wait until
}); // ends install event

Activate Event #

Activate will perform cleanup on the caches.

If there is a cache that doesn't exist in the Array that we pass to the event then it's deleted This is the reason why we went through such a convoluted way to define the caches. We can change the names individually and then they'll get deleted the next time the user visits the site.

self.addEventListener('activate', (event) => {
  clients.claim();
  event.waitUntil(
  caches.keys().then((keys) => {
    Promise.all(keys.map((key) => {
      // if the cache is not one in the list
      if (!expectedCaches.includes(key)) {
        // delete it
        return caches.delete(key);
      }
    }))
    .then(() => {
      console.log('Everything cleaned up');
    });
  }));
});

Fetch Event #

The fetch event is where most of the work will happen. Unlike the work in Workbox service worker, we don't break the routes into different blocks in the vanilla service worker.

There are some aspects where the vanilla service worker still doesn't match the Workbox implementation. I'm working on expiration d

All the following sections are inside the fetch event listener. I've broken them down for ease of reading.

This is an important item to remember for all fetch handlers that put resources in caches using the cache API:

Put a copy of the response in the cache, otherwise the code will throw an exception because the response is a stream that can only be consumed once

When we define the event we do two things right away:

We define a constant to hold the values of the event's request object.

If the request doesn't match the GET HTTP method we return without doing anything. The service worker will only work with GET requests.

self.addEventListener('fetch', (event) => {
  const request = event.request;

  if (request.method !== 'GET') {
    return;
  }

The first handler is for fonts. Since the fonts are requested from the typography-*.css stylesheets I have to make sure that the font is loaded from the stylesheet or that the file ends in one of the four font formats I work with.

  if (request.url.match(/\.(ttf|otf|woff|woff2)$/) ||
    (request.referrer.includes('typography'))) {
    event.waitUntil(
      // Open the fonts cache
      caches.open(FONTS)
        .then((cache) => {
          // return the font to the user
          return cache.match(request);
        })
        .then((response) => {
          // Open the cache
          caches.open(FONTS)
          .then((cache) => {
            // Fetch the resource from the network
            return fetch(request)
            .then((response) => {
              // Put a copy of the resource in the cache
              return cache.put(request, response.clone())
              .then(() => {
                return response;
              });
            });
          });
        })
    );
  }

Caching Javascript resources has a different objective: To cache all the Javascript files that are not in the install precache. The match query means to include all files with a .js extension except one that includes zenscroller in the URL (Zenscroller is cached at install).

  if (request.url.match(/\.(js)$/) && (!request.url.includes('zenscroll'))) {
    event.waitUntil(
      caches.open(JS)
      .then((cache) => {
        return cache.match(request);
      })
      .then((response) => {
        caches.open(JS)
        .then((cache) => {
          fetch(request)
          .then((response) => {
            return cache.put(request, response.clone())
              .then(() => {
                // Return the response
                return response;
              });
          });
        });
      })
    );
    return;
  }

For CSS we want to make sure we cache all files with a .css extension and that are not fonts (denoted by a .woff2 extension).

  // two places. Working on figuring out a solution
  if (request.url.match(/\.(css)$/) &&
    !request.url.includes('woff2')) {
    event.waitUntil(
      // Open the content cache
      caches.open(CSS)
        .then((cache) => {
          // return the CSS to the user
          return cache.match(request);
        })
        .then((response) => {
          // Open the cache
          return caches.open(CSS)
          .then((cache) => {
            // Fetch the resource from the network
            fetch(request).then((response) => {
              // Put a copy of the response in the cache
              return cache.put(request, response.clone())
              .then(() => {
                // Return the response
                return response;
              });
            });
          });
        })
    );
    return;
  }

In the cache for images, we want to make sure that we add only images only (those that match jpeg, jpg, png, gif and svg) and not the assets that reference the images (HTML and CSS).

If the image is not in the cache and we can't retrieve it from the network we provide a local fallback as a new response using an SVG image.

  if (request.url.match(/\.(jpe?g|png|gif|svg)$/) &&
    (!request.url.match('/\.(html|css)$/'))) {
    event.waitUntil(
      caches.open(IMAGES)
      .then((cache) => {
        // return the IMAGES to the user
        return caches.match(request);
      })
      .then((response) => {
        // Open the cache
        return caches.open(IMAGES)
        .then((cache) => {
          // Fetch the resource from the network
          fetch(request)
          .then((response) => {
            // Put a copy of the response in the cache
            return cache.put(request, response.clone())
            .then(() => {
              // Return the response
              return response;
            });
          })
          .catch((error) => {
            return new Response(OFFLINESVG, {
              headers: {
                'Content-Type': 'image/svg+xml',
              },
            });
          });
        });
      })
    );
    return; // If we get to here, bail out
  }

The final handler for assets is for HTML content by using a 'stale while revalidate' strategy, we return the resource in the cache, if it's not in the cache then we fetch it from the network, store a copy of the response in the cache and return the resource to the user.

In this cache we could be more sophisticated and return different responses based on whether we're offline, the resource was not found or any other network failure. But for an MVP, this is enough.

  if (event.request.headers.get('Accept')
    .includes('text/html')) {
    // Open the content cache
    caches.open(CONTENT)
    .then((cache) => {
      // return the content to the user
      return caches.match(request);
    })
    .then((response) => {
      // Open the cache
      caches.open(CONTENT)
      .then((cache) => {
        // Fetch the resource from the network
        fetch(request)
        .then((response) => {
          // Put a copy of the response in the cache
          cache.put(request, response.clone())
          .then(() => {
            // Return the response
            return response;
          });
        });
      });
    });
  }
});

Edit on Github