First look at service worker

A service worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don't need a web page or user interaction.

More precisely, service worker is used for boosting the user experience of a web application, which allows you to programmatically managing a cache of responses and support offline experiences, hence giving developers complete control over web from client side.

You might reference google web (opens new window) for official details.

In this article, I'm going to re-state what I think it is important for service worker and how to playaround with it in a demo web.

# Concept

Service Worker lifecycle

There are few state of service worker which I think matters in the demo below:

  • installing

    • with event install
    • this happens when the browser detect there's a byte difference between the registered service worker javascript file
  • activated

    • with event activate
    • this happens after a new service worker installed. However, if the old page is still active, which means the old service worker still in use, the service worker will waiting for activate until old page is closed (old service worker is terminated) and hence trigger activate event become activated.
  • fetch

    • with event fetch
    • executed for the resources fetch

# Setup

# Debugging tool

So far, chrome is supporting a native service worker debugging tool, which you might find it in chrome://inspect/#service-workers. Firefox don't have native tool for it, you might need search for a plugin to try out.

Only installed service worker can be viewed from this tool, hence make sure you have at least one service worker installed to your browser before visit that page. Example:

Service Worker debug tool

# Service worker demo

Demo file structure

$ tree
.
├── app.js
├── file_cache_on_install
│   ├── img.jpg
│   └── install.json
├── file_runtime_cache
│   ├── img.jpg
│   └── runtime.json
├── index.html
└── sw.js

2 directories, 7 files
1
2
3
4
5
6
7
8
9
10
11
12
13

index.html, the entry point of the web app

<html>
  <head>
    <title>Web workers</title>
    <style>
      img {
        width: 25%;
      }
      * {
        box-sizing: border-box;
      }

      /* Create two equal columns that floats next to each other */
      .column {
        float: left;
        width: 50%;
        padding: 10px;
        height: 300px; /* Should be removed. Only for demonstration */
      }

      /* Clear floats after the columns */
      .row:after {
        content: '';
        display: table;
        clear: both;
      }
    </style>

    <script src="app.js">
      // app.js is mainly used for register service worker to this html
    </script>

    <script type="text/javascript">
      // this is used for load json file and display to html for display
      function loadFileToId(filePath, idName) {
        fetch(filePath)
          .then(function(response) {
            return response.json();
          })
          .then(function(mydata) {
            var div = document.getElementById(idName);

            for (var i = 0; i < mydata.length; i++) {
              div.innerHTML =
                div.innerHTML +
                "<p class='inner' id=" +
                i +
                '>' +
                mydata[i].name +
                '</p>';
            }
          });
      }

      function load() {
        loadFileToId('./file_cache_on_install/install.json', 'on_install_data');
        loadFileToId('./file_runtime_cache/runtime.json', 'on_runtime_data');
      }
    </script>
  </head>
  <body onload="load()">
    <div class="row">
      <div class="column" style="background-color:#aaa;">
        <h2>JSON on install</h2>
        <div id="on_install_data"></div>
      </div>
      <div class="column" style="background-color:#bbb;">
        <h2>JSON on runtime</h2>
        <div id="on_runtime_data"></div>
      </div>
    </div>

    <img src="file_runtime_cache/img.jpg" />
    <img src="file_cache_on_install/img.jpg" />
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

above file is linked to app.js as a main script, which register a service worker:

console.log("What's in navigator:", navigator);
console.log("'serviceWorker' in navigator", 'serviceWorker' in navigator);

if ('serviceWorker' in navigator) {
  const serviceWorkerJsFilePath = './sw.js';
  // simply register a service worker
  window.addEventListener('load', function() {
    navigator.serviceWorker.register(serviceWorkerJsFilePath).then(
      function(registration) {
        // Registration was successful
        console.log(
          `ServiceWorker registration successful from ${serviceWorkerJsFilePath} with scope: ${registration.scope}`
        );
      },
      function(err) {
        // registration failed :(
        console.log('ServiceWorker registration failed: ', err);
      }
    );
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

while the service worker is defined in sw.js:

// decide what you wanna cache as initial steps to register the service worker
// once the service worker js is updated, install will be triggered
this.addEventListener('install', onInstall);

// a trigger which will get executed after service worker install
// if current page still open, even new service worker installed, it wont enter activate state
// instead, it will enter waiting state
// once old page is closed, new service worker enter activate and hence trigger the handler below
this.addEventListener('activate', onActivate);

// all the resource fetch action go through here
this.addEventListener('fetch', onFetch);

var urlsToCache = [
  'file_cache_on_install/install.json',
  'file_cache_on_install/img.jpg'
];

var INSTALL_CACHE_NAME = 'file_cache_on_install';
var RUNTIME_CACHE_NAME = 'file_cache_on_runtime';

function onInstall(event) {
  console.log('INSTALL service worker...');
  event.waitUntil(
    caches.open(INSTALL_CACHE_NAME).then(function(cache) {
      console.log(
        'INSTALL -> URLs to cache during install service worker',
        urlsToCache
      );

      // magic cache handler, which cache the url response to predefined urls group
      return cache.addAll(urlsToCache);
    })
  );
}

function onActivate(event) {
  console.log('ACTIVATE service worker...');

  // let s delete all the cache which is not in the on install cache
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames
          .filter(function(key) {
            console.log(
              `ACTIVATE -> key:${key}, (key !== INSTALL_CACHE_NAME):${key !==
                INSTALL_CACHE_NAME}`
            );
            return key !== INSTALL_CACHE_NAME;
          })
          .map(function(cacheName) {
            console.log('ACTIVATE -> delete cache:', cacheName);
            return caches.delete(cacheName);
          })
      );
    })
  );
}

function onFetch(event) {
  console.log('FETCH service worker...');
  // process including on install cache resources and the rest resources
  // 1. fetch resource from on install cache store, if cache not here, continue
  // 2. fetch resource from network and cache back to another cache store
  // 3. if second steps failed, fetch the resources from cache (offline support)
  // NOTE: no handling if cannot get data from both cache & network
  event.respondWith(fetchFromInstallCacheOrFallback(event));
}

// fetch resource from on install cache store, if we have cache return and exits
function fetchFromInstallCacheOrFallback(event) {
  return caches.open(INSTALL_CACHE_NAME).then(cache => {
    return cache.match(event.request).then(response => {
      console.log('event.request', event.request, 'response', response);
      if (response) {
        return response;
      }

      // return if we have network
      return fetchFromNetworkOrFallback(event);
    });
  });
}

// fetch resource from network
// if resource response correctly, cache response to cache store, return and exist
function fetchFromNetworkOrFallback(event) {
  return fetch(event.request)
    .then(response => {
      // Check if we received a valid response
      console.log('FETCH -> Response from network:', response);
      if (!response || response.status !== 200) {
        return response;
      }

      // better make a clone of response
      var responseToCache = response.clone();

      caches.open(RUNTIME_CACHE_NAME).then(function(cache) {
        console.log(
          `FETCH -> Caching ${event.request.url} to ${RUNTIME_CACHE_NAME}, for response ${response}`
        );
        cache.put(event.request, responseToCache);
      });

      return response;
    })
    .catch(() => {
      return fetchFromRuntimeCache(event);
    });
}

// fetch resource from cache store, return proper response of exists
function fetchFromRuntimeCache(event) {
  return caches.open(RUNTIME_CACHE_NAME).then(cache => {
    return cache.match(event.request).then(function(response) {
      // Cache hit - return response
      console.log(
        `FETCH -> Cached request from ${event.request.url}, for response ${response}`
      );
      return response;
    });
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125

beyond, there are some testing file, file_cache_on_install/install.json:

[
  { "name": "On install" },
  { "name": "Arun" },
  { "name": "Sunil" },
  { "name": "Rahul" },
  { "name": "Anita" }
]
1
2
3
4
5
6
7

file_runtime_cache/runtime.json:

[
  { "name": "On runtime" },
  { "name": "Sunil" },
  { "name": "Rahul" },
  { "name": "Anita" }
]
1
2
3
4
5
6

if you serve it out locally

$ python -m SimpleHTTPServer 5000
1

you might get view:

Service Worker demo look

# Demo rundown

# Normal setup and check cache

serve the web application and go to localhost:5000

$ python -m SimpleHTTPServer 5000
1

you can view the app properly with resources fetch, the cache are listed as:

Service Worker cache storage

# Offline mode with resource fetch from cache store

If you properly fetched the web page. Then, enable offline mode and refresh your browser, you should still be able to see the web

Service Worker offline refresh with cache

TIP

  • if you delete the cache from the cache store and refresh again, you will not be able to visit the web, entire cache store will be gone as well.
  • if you turn the offline mode off, refresh again, you will be able to fetch entire web page and have them cached

# Always fetch from network for runtime cache

Once you load the web page, make a change to:

  • file_runtime_cache/runtime.json (e.g. one more item with name)
[
  { "name": "On runtime" },
  { "name": "Marvin" },
  { "name": "Sunil" },
  { "name": "Rahul" },
  { "name": "Anita" }
]
1
2
3
4
5
6
7

Reload the page, you will see the new item pop up to the web page

Service Worker refresh update update runtime json

# Install only happen for new service worker

Once you load the web page, make a change to:

  • file_cache_on_install/install.json (e.g. one more item with name)

Reload the page, there's no change happen to the web page.

Make a change to the sw.js and reload the page, e.g. add a new line to the end of the file

Service Worker refresh with sw update

TIP

  • old service worker still processing while new service worker is waiting for activating

  • from the console log, you can see install event is triggered, hence the file_cache_on_install/install.json finally updated to the cache store

  • at this point, the display is still same content as before for JSON on install field

  • reload the page once again, you can proper fetch the resources from install cache, hence the UI updated accordingly

  • open a new page and look at the service worker activity and close the old page, you can see:

    • old service worker is terminated
    • new service worker is activated (although it is stopped)

Service Worker old web page closed

# Conclusion

With Service worker, engineer will be more flexible and over control on client's browser to tuning the user experience in an offline web mode or with poor network connectivity

Worth a look in the future:

  • Service worker with PWA
Start with Flutter for mobile development
Host NATS for message bus

Host NATS for message bus

# What is NATS ?

NATS messaging enables the exchange of data that is segmented into messages among computer applications and services. These messages are addressed by subjects and do not depend on network location. This provides an abstraction layer between the application or service and the underlying physical network. Data is encoded and framed as a message and sent by a publisher. The message is received, decoded, and processed by one or more subscribers.