- PWA News
- Introducing Service Workers
- Preparing to Code
- Registering a Service Worker
- Service Worker Scope
- The Service Worker Lifecycle
- Wrap-Up
The Service Worker Lifecycle
Each service worker cycles through multiple states from the moment the browser calls navigator.serviceworker.register until it’s discarded or replaced by the browser. The states defined in the service worker specification are
Installing
Waiting
Active
When an app registers a service worker, the browser
Locates the service worker (requests it from a server)
Downloads the service worker
Parses and executes the service worker
If the browser can’t download or execute the service worker, it discards the service worker (if it has it) and informs the code that called navigator.serviceworker.register. In Listing 3.3, that means that the catch clause executes and whatever code is there executes.
If the browser successfully downloads the service worker, it executes the service worker, and that’s how the service worker registers the install and activate event listeners in the service worker code.
At this point, the install event listener fires; that’s where a service worker creates its local cache and performs any additional setup steps. A representation of the service worker (version 1) at the Installing state is shown in Figure 3.9.
Figure 3.9 Service Worker 1 Installing
If a service worker isn’t currently registered, the service worker transitions to the Active state, and it’s ready to process fetch requests the next time the app reloads, as shown in Figure 3.10.
Figure 3.10 Service Worker 1 Active
If the web app attempts to install a new version of the service worker (version 2, for example), then the process starts all over again for the new service worker. The browser downloads and executes service worker v2, the v2 install event fires, and it completes its initial setup.
At this point, the app is still active and has an active service worker (v1) in play. Remember, the current service worker remains active until you close the browser (or at least all tabs running the web app) or the browser is configured to force reloading the service worker. With an existing service worker active, service worker v2 goes into a waiting state, as shown in Figure 3.11.
Figure 3.11 Two Service Workers in Play
Once the user closes all browser tabs running the app or restarts the browser, the browser discards service worker v1 and sets service worker v2 to active as shown in Figure 3.12.
Figure 3.12 Service Worker V2 Active
When you update the service worker and a user navigates to the app, the browser attempts to download the service worker again. If the newly downloaded service worker is as little as one byte different from the one currently registered, the browser registers the new service worker and the activation process kicks off again. Regardless of whether or not it has changed, the browser downloads the service worker every time the page reloads.
Forcing Activation
Earlier I described ways to force the browser to activate a service worker by reloading the page or enabling the Reload option in the browser developer tools. Both of those options are great but require some action by the user. To force the browser to activate a service worker programmatically, simply execute the following line of code somewhere in your service worker:
// force service worker activation self.skipWaiting();
You’ll typically perform this action during the install event, as shown in Listing 3.7.
Listing 3.7 Fourth Service Worker Example: sw-37.js
self.addEventListener('install', event => { // fires when the browser installs the app // here we're just logging the event and the contents // of the object passed to the event. the purpose of this event // is to give the service worker a place to setup the local // environment after the installation completes. console.log(`SW: Event fired: ${event.type}`); console.dir(event); // force service worker activation self.skipWaiting(); }); self.addEventListener('activate', event => { // fires after the service worker completes its installation. // It's a place for the service worker to clean up from previous // service worker versions console.log(`SW: Event fired: ${event.type}`); console.dir(event); }); self.addEventListener('fetch', event => { // fires whenever the app requests a resource (file or data) console.log(`SW: Fetching ${event.request.url}`); // next, go get the requested resource from the network, // nothing fancy going on here. event.respondWith(fetch(event.request)); });
Claiming Additional Browser Tabs
In some cases, users may have multiple instances of your app running in separate browser tabs. When you register a new service worker, you can apply that new service worker across all relevant tabs. To do this, in the service worker’s activate event listener, add the following code:
// apply this service worker to all tabs running the app self.clients.claim()
A complete listing for a service worker using this feature is provided in Listing 3.8.
Listing 3.8 Fifth Service Worker Example: sw-38.js
self.addEventListener('install', event => { // fires when the browser installs the app // here we're just logging the event and the contents // of the object passed to the event. the purpose of this event // is to give the service worker a place to setup the local // environment after the installation completes. console.log(`SW: Event fired: ${event.type}`); console.dir(event); // force service worker activation self.skipWaiting(); }); self.addEventListener('activate', event => { // fires after the service worker completes its installation. // It's a place for the service worker to clean up from previous // service worker versions console.log(`SW: Event fired: ${event.type}`); console.dir(event); // apply this service worker to all tabs running the app self.clients.claim() }); self.addEventListener('fetch', event => { // fires whenever the app requests a resource (file or data) console.log(`SW: Fetching ${event.request.url}`); // next, go get the requested resource from the network, // nothing fancy going on here. event.respondWith(fetch(event.request)); });
Observing a Service Worker Change
In the previous section, I showed how to claim service worker control over other browser tabs running the same web app after a new service worker activation. In this case, you have at least two browser tabs open running the same app, and in one tab a new version of the service worker was just activated.
To enable the app running in the other tabs to recognize the activation of the new service worker, add the following event listener to the bottom of the project’s sw.js file:
navigator.serviceWorker.addEventListener('controllerchange', () => { console.log("Hmmm, we’re operating under a new service worker"); });
The service worker controllerchange event fires when the browser detects a new service worker in play, and you’ll use this event listener to inform the user or force the current tab to reload.
Forcing a Service Worker Update
In the world of single-page apps (SPAs), browsers load the framework of a web app once, and the app then swaps in the variable content as often as needed while the user works. These apps don’t get reloaded much because users simply don’t need to. This is kind of a stretch case, but if you’re doing frequent development on the app or know you’re going to update your app’s service worker frequently, the service worker’s registration object (reg in all the source code examples so far) provides a way to request an update check from the app’s code
To enable this, simply execute the following line of code periodically to check for updates:
reg.update();
The trick is that you must maintain access to the registration object long enough that you can do this. In Listing 3.9, I took the service worker registration code from Listing 3.2 and modified it a bit.
First, I created a variable called regObject, which the code uses to capture a pointer to the reg object exposed by the call to navigator.serviceWorker.register. Next, I added some code to the registration success case (the .then method) that stores a pointer to the reg object in the regObject variable and sets up an interval timer for every 10 minutes. Finally, I added a requestUpgrade function that triggers every 10 minutes to check for a service worker update.
Listing 3.9 Alternate Service Worker Registration: sw-reg2.js
// define a variable to hold a reference to the // registration object (reg) var regObject; // does the browser support service workers? if ('serviceWorker' in navigator) { // then register our service worker navigator.serviceWorker.register('/sw.js') .then(reg => { // display a success message console.log(`Service Worker Registration (Scope: ${reg.scope})`); // Store the `reg` object away for later use regObject = reg; // setup the interval timer setInterval(requestUpgrade, 600000); }) .catch(error => { // display an error message let msg = `Service Worker Error (${error})`; console.error(msg); // display a warning dialog (using Sweet Alert 2) Swal.fire('Registration Error', msg, 'error'); }); } else { // happens when the app isn't served over a TLS connection // (HTTPS) or if the browser doesn't support service workers console.warn('Service Worker not available'); // we're not going to use an alert dialog here // because if it doesn't work, it doesn't work; // this doesn't change the behavior of the app // for the user } function requestUpgrade(){ console.log('Requesting an upgrade'); regObject.update(); }
You could even trigger execution of this code through a push notification if you wanted to force the update only when you publish updates by sending a special notification message whenever you publish a new version of the service worker.
Service Worker ready Promise
There’s one final service worker lifecycle topic I haven’t covered yet. The serviceWorker object has a read-only ready property that returns a promise that never rejects and sits there waiting patiently until the service worker registration is active. This gives your service worker registration code a place to do things when a page loads with an active service worker.
We already have the install and activate events, both of which get involved during service worker registration and replacement. If your app wants to execute code only when a service worker is active, use the following:
if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then((reg) => { // we have an active service worker working for us console.log(`Service Worker ready (Scope: ${reg.scope})`); // do something interesting... }); } else { // happens when the app isn't served over a TLS connection (HTTPS) console.warn('Service Worker not available'); }
You’ll see an example of this in action in Chapter 6.