The Philly Code Camp ⚙️ Service Worker ⚙️ - Making the Schedule Work 📶 Offline! 📱 [A Service Worker Tutorial]

philly-cc-iphone-x-viewEarlier this week I launched the Philly Code Camp Progressive Web App. I explained how the application works from a high level but today I want to go over how the service worker is structured.

To be a progressive web app (PWA) a website must be secure, have a valid web manifest file and most importantly register a service worker. Using HTTPS and having a valid web manifest file a rather simple features that can be added in an hour to in most cases.

But a good service worker, that's a whole different thing.

Recently I have been launching a cadre of new simple progressive web apps just to show how simple it is to make a PWA. But none of these apps, so far, have really used a serviceWorker to its full capabilities.

The Philly Code Camp service worker starts showing how you can use a service worker to build a rich front end user experience with nothing but web technologies.

What a Service Worker Is

Before I dive into the code let's talk a little about what a service worker is.

The service worker is called a proxy between the browser and the network. That's because it doesn't run in the same process as the browser tab use to render a page.

It is an event driven, a synchronous utility that executes in the background. Right now it's capabilities are caching network addressable resources, native push notifications and background synchronization.

service-workers-proxy-to-network

The Philly Code Camp progressive web app utilizes service worker cache to make an instant loading and off-line capable user experience. You should be able to use this as a simple reference to get you started with service workers and service worker caching.

The big feature I will demonstrate in this article is a way to dynamically render pages when off-line. In the past this was not natural, but could be done.

Before service workers were created, I started leveraging appCache combined with localStorage to do something very similar. But service workers have made that technique native, and more robust.

The Service Worker Setup

The Philly Code Camp service worker starts off by implementing the strict mode that I talked about in a previous article. It then imports external libraries using the importScripts method.

importScripts has been around with web workers for several years. This is a great way to pull an external libraries to add more value to your worker. You can also extract your service worker logic into an external file. For more advanced service workers I highly recommend this technique.

One gotcha you need to be aware of is how scripts are updated.

When a script is imported using importScripts is not updated when the service worker is updated. These scripts are updated typically in accordance to browser cache. Please note that this is not the service worker cache.

This means it can be a little frustrating when you update these external files because they don't always get updated in the service worker.

Common cache posting techniques are a way to get around this. These include appending a hash or timestamp in a query string to the URL, or renaming the file using the content hash.

Another way that I found helpful while developing is to unregister the service worker in the developer tools and let it reregister. This will force the importScripts method to reload those files.

For the Philly Code Camp service worker I import localForage, Mustache and the session abstraction module.

LocalForage is a library that makes using IndexedDB work much like localStorage. The advantages it makes localStorage API promise based. And if you're wondering localStorage is not available inside of a service worker because it is not asynchronous. This means you should use indexedDB instead.

IndexedDB is not an easy API to work with, so you generally use an abstraction library to make it more approachable. For my purposes localForage has been the easiest to work with so far. Your needs could vary in a different library may be better.

I wrote about mustache and another recent article. It is a great little library that allows you to easily render HTML or any type of text from a template and a JSON object.

I'll review the session library in a different article so I won't go over it here. But it manages the applications session data for us. And because it has no UI coupling it can be used in the service worker.

The Service Worker Constants

The next section is a declaration of constants. These are variables used in the service worker that don't change.

I found it to be a best practice when developing a service worker to always declare a version variable. This allows you to increment your service worker as you continue to work on it.


const version = "1.01",
    preCache = "PRECACHE-" + version,
    dynamicCache = "DYNAMIC-" + version,
    cacheList = [
        "/",
        "img/phillydotnet.png",
        "js/app/app.js",
        "js/app/sessions.js",
        "js/libs/localforage.min.js",
        "js/libs/mustache.min.js",
        "js/libs/utils.js",
        "css/libs/bootstrap.min.css",
        "css/libs/fontawesome-all.css",
        "css/app/site.css",
        "css/webfonts/fa-solid-900.woff2",
        "api/philly-cc-schedule.json",
        "html/app-shell.html",
        "templates/session-list-item.html",
        "templates/session.html"
    ];

The next two variable declarations are the names of our caches, a pre-cache and a dynamic cache. Both have their core name but I also append the version value. This makes managing caches much simpler as you will see when I review the activate event.

The last constant is an array of the URLs that should be pre-cached when the service worker is first registered. For the Philly Code Camp site this is all of the asset dependencies like JavaScript, CSS and images.

I cacheed the home page and three template HTML files. I'll explain those files later in the article.

Service Worker Life Cycle Events

The next section are what I call the life cycle events, install and activate.

The install event triggers when a service worker is first registered. The service worker life cycle is a very complex topic into broad for this article. I have about two hours worth of content on this topic in my progressive web app course.


self.addEventListener("install", event => {

self.skipWaiting();

caches.open(preCache)
    .then(function (cache) {

        cache.addAll(cacheList);

    });

});

There are typically two things you do in a service worker install event handler, call skipWaiting if you want the service worker to immediately become active and precache application assets.

I do both in this installment handler. You should be careful when using skipWaiting because you might break your progressive web app experience if the service worker update is drastically different from the previous version.

To precache the assets and make them available immediately you simply need to open a reference to your precache cache and then pass the array defined in the constant section to the at all method. This method will take care of fetching those assets and placing those in the precache.

The second phase of a service worker registration is when it actually becomes active. A new service worker does not immediately become active because it may potentially break the user experience.

Therefore the activate event will fire when the service worker does become life.


self.addEventListener("activate", event => {

event.waitUntil(

    caches.keys().then(cacheNames => {
      cacheNames.forEach(value => {

        if (value.indexOf(version) < 0) {
          caches.delete(value);
        }

      });

      console.log("service worker activated");

      return;

    })

  );

});

Again there are many things you can do in this event, but the most common task is to clean up old caches. This is where that version value comes into play.

In the service worker I loop through all of the named caches and check to see if they contain the current version number. If they don't I delete the previous caches.

This is a way to ensure that you don't have old's, stale responses cached. It also means that you don't run out of room to cache assets for your application.

Cache invalidation is a very complex topic. I wrote an article for Perth planet this past December going over some concepts here. I encourage you to read it if you want to know more about the topic.

The Fetch Event Handler

The service worker fetch event handler fires anytime there is a request for a network addressable resources, or a file from the server.

The event receives an event object which has a request property. You can use this request object to see if there is a cached response available.


self.addEventListener("fetch", event => {

event.respondWith(
    caches.match(event.request)
        .then(function (response) {

            if (response) {
                return response;
            }

            return fetch(event.request)
                .then(response => {

                    if (response.ok) {

                        //I have no clue why the chrome extensions requests are passed through the SW
                        //but I don't like the error messages in the console ;)
                        if (event.request.url.indexOf("chrome-extension") === -1) {

                            let copy = response.clone();

                            //if it was not in the cache it must be added to the dynamic cache
                            caches.open(dynamicCache)
                                .then(cache => {
                                    cache.put(event.request, copy);
                                });

                        }

                        return response;

                    }

                }).catch(err => {

                    if (err.message === "Failed to fetch") {

                        if (event.request.url.indexOf("session") > -1) {

                            return renderSession(event);

                        }

                    }

                });
        })
);

});

To do this call the caches object match method, passing the request object. This method will loop through all the named caches and see if there is a response cached for this specific request.

If there is a response it will return it.

You should check to see if the response object exist, and if so return that to the client.

For the Philly Code Camp progressive web app I chose to use a cache first, then network falling back response pattern. This means I'm looking at the cache first and if there is nothing in the cache then I make a request to the server. If were off-line I will check to see if I can render it locally or not.

If there is not a response a new fetch request is made. The request object is passed to the fetch method and the network request is initiated.

When this returns you should have a response object. If the request was good, which means it has a status of 200 which is also represented by the response.okay property, then you can process it.

The first thing I want to do is make sure that I cacheed this response so I don't need to hit the network the next time. To do this you need to clone the response object. You cannot use a response object more than once.

I then cache the cloned response object in the dynamic cache. To do this you open up a reference to the dynamic cache and then pass the request object and the response copy to the put method.

Now there is a cached response for that request.

As a side note I learn to also add a check to see if the request is part of a chrome extension request. If it is I don't cache the response. I'm not sure why chrome tries to do this, because extensions should be running in a separate process. But if you don't you will see funky exceptions happening.

Handling Offline

The real cool thing about this particular service worker is how I handle off-line mode.

For the application home page nothing really changes because everything is pre-cached. If you noticed one of the assets that I made sure was precache was the entire code camp schedule.

The schedule is a Jason object which makes it easy to manipulate locally. I demonstrated this by showing you how I do search and faceted filtering. But because the entire schedule is local I can also dynamically render individual session pages.

But to be able to do that I needed to also precache the app shell and the session detail template.

If the device is off-line in a request to the network is made an exception is thrown. This is why there is a catch event handler in the service worker. But I only have logic to dynamically render a session detail page. So I check the request URL to see if a request was made to a session. If so I then trigger the logic to render the session inside the service worker.

If you watch the video on how the application built script works I essentially extracted that logic and placed it in the service worker.


function renderSession(event) {

let slug = getSlug(event.request.url),
    appShell = "",
    sessionTemplate = "";

return getAppShell()
    .then(html => {

        appShell = html;

    })
    .then(() => {

        return getSessionTemplate()
            .then(html => {

                sessionTemplate = html;

            });

    }).then(() => {

        let sessionShell = appShell.replace("<%template%>", sessionTemplate);

        return getSessionBySlug(slug)
            .then((session) => {

                let sessionPage = Mustache.render(sessionShell, session);

                //make custom response
                let response = new Response(sessionPage, {
                    headers: {
                        'content-type': 'text/html'
                    }
                }),
                    copy = response.clone();

                caches.open(dynamicCache)
                    .then(cache => {
                        cache.put(event.request, copy);
                    });

                return response;

            });

    });

};

The renderSession function does several of things: it loads the app shell and session templates and then request the session detail. Once it has retrieved all three of these files it then uses mustache to render the HTML that composes the entire page.

Once the pages rendered it is then cached and returned to the client. I use the same logic as if it came from the network, clone the response and cache that version.

The big difference here is I create a custom response object. This is one of the cool things about the fetch API, you can create custom request and response objects.

By caching the rendered response the next time this session is requested it will come from cache and not need to hit the network or the dynamically rendered. Even if the device goes back online the cached response from this process will be available.

Wrapping Things Up

All this quick explainer of the Philly Code Camp service worker has been very helpful and inspirational.

Service workers are extremely broad topic to cover. And even though I touched on a few intermediate and advanced concepts in the service worker it by no means demonstrates the full breadth of service workers.

My Progressive Web App from Beginner to Expert course has at least a dozen hours of video training on service worker development. A more will be posted in the coming weeks.

Handling the service worker life cycle and the proper caching strategies for your application are can be very complex and require a very educated developer to execute properly.

You should be able to use the Philly Code Camp service worker as a decent starting point for many applications. Just know that your application's personality may dictate a slightly different path.

If you want to learn how to do service worker development I highly encourage you to register for my progressive web application course. You can sign up right now for just $29 and save hundred and 71 off the regular list price.

The Philly Code Camp PWA Source Code is on GitHub.

Service workers are great super power that makes the web a great place to build rich front-end experiences. A well-done service worker can be the key separates your website from the competition. It can also make your business more profitable and productive because your applications execute cleaner and faster.

Share This Article With Your Friends!

Googles Ads Facebook Pixel Bing Pixel LinkedIn Pixel