banner



How To Debug An Unknown Error Occurred When Fetching The Script Service Worker

How to Gear up the Refresh Button When Using Service Workers

Dan Fabulich is a Principal Engineer at Redfin. ( We're hiring !)

In a previous commodity, I explained how and why Service Workers pause the browser'due south Refresh push button past default.

But nobody likes a whiner! In this article, I'll certificate how to set up the Refresh button.

Getting this correct requires an intimate understanding of the Service Worker lifecycle, the Caches API, the Registration API, and the Clients API. By the time you've plowed through this commodity, you'll know pretty much everything you need to know well-nigh the Service Worker API.

Hash out on Hacker News
Discuss on Reddit

Step I: Read my previous article

My previous article on refreshing Service Workers provides a lot of valuable background textile. I'll attempt to summarize it in this department, simply I think you'll probably simply have to read the whole thing kickoff, because Service Workers are rocket science.

Here's the key insight:

v1 tabs tightly couple to their v1 Cache; v2 tabs tightly couple to their v2 Cache. This tight coupling makes them "application caches." The app must be completely shut down (all tabs closed) in club to upgrade atomically to the newest enshroud of code.

Service Workers are like apps. You can install them, outset them, finish them, and upgrade them.

You lot tin can't safely upgrade an app (or a Service Worker) while it's still running; you need to shut the erstwhile version down start, ensuring what I telephone call "code consistency." In add-on, since new versions of an app may change the schema of client-side information, information technology'due south important to run only i version of the app at a time, ensuring "data consistency."

Service Workers can install a packet of files atomically with the install effect, so run data migration during the activate event. Past default, once a v1 Service Worker activates, the v2 version of the Service Worker can install but volition refuse to activate, "waiting" until the one-time v1 Service Worker dies.

The onetime v1 Service Worker won't die until all of the v1 Service Worker's tabs have closed. (The Service Worker API calls tabs "clients.")

While the v1 Service Worker lives, the Refresh button will generate pages from the old v1 Service Worker's cache, no thing how many times we refresh.

Allow's ready this, shall we?

Four Ways to Solve the Same Problem

Here are four unlike approaches to fixing the Refresh push and forcing an update, each associated with a different phase of developer mindfulness and intellectual development.

Approach #1: We can simply skip waiting (but beware)

"For every complex trouble at that place is an answer that is clear, simple, and wrong." — H. Fifty. Mencken

The simplest and nearly dangerous approach is to just skip waiting during installation. There'due south a 1-line role in the global scope of all Service Workers, skipWaiting(), that will do this for us. When nosotros employ it, the new v2 Service Worker will immediately kill the old v1 activated Service Worker one time the v2 Service Worker installs.

But beware: this approach eliminates whatever guarantees of code consistency or data consistency.

The new v2 Service Worker will actuate and delete the erstwhile v1 Enshroud while a v1 tab is still open up. What happens if the v1 tab tries to load /v1/styles.css at that point?

At best, the v1 tab will attempt to load our style canvas from the network, which is a waste of bandwidth, because we just discarded a perfectly proficient cached re-create of the file. At worst, the user might be offline at the time, resulting in a broken v1 tab, but the user won't know why.

The worst thing nigh blindly calling skipWaiting() is that it appears to work perfectly at first, but it results in bugs in product that are hard to understand and reproduce.

Approach #2: Refresh onetime tabs when a new Service Worker is installed

This is just a slight tweak on the previous approach. navigator.serviceWorker is a ServiceWorkerContainer object; it has a controllerchange event which fires when a new Service Worker takes control of the current page.

So suppose we skipWaiting() during installation and add lawmaking like this in the web folio:

          navigator.serviceWorker.addEventListener('controllerchange',
function() { window.location.reload(); }
);

That does work. When the v2 Service Worker skips waiting to have control, any v1 tab will automatically refresh, turning it into a v2 tab.

But it's not a proficient user experience to trigger a refresh when the user's not expecting it. If the folio refreshes while the user is filling out an lodge form for styptic pencils, yous could lose data, coin, and/or blood. 💔

UPDATE: If you use this technique, use a variable to make sure yous refresh the page only one time, or you'll cause an infinite refresh loop when using the Chrome Dev Tools "Update on Reload" feature.

          var refreshing;
navigator.serviceWorker.addEventListener('controllerchange',
function() {
if (refreshing) return;
refreshing = true;
window.location.reload();
}
);

Arroyo #3: Allow the user to control when to skip waiting with the Registration API

In this approach, nosotros'll wait for a new Service Worker to install, and and then we'll pop upwards a UI message in the page, prompting the user to click an in-app "Refresh" link.

We'll even so configure the page to refresh when the controllerchange event fires, as in Approach #2 — but in this approach, the event will burn when the user asks for a refresh, and so we know the user won't be interrupted.

Oh, joy, another Service Worker API: Registration API

To listen for a new Service Worker, we'll need to use a ServiceWorkerRegistration object. In our "naive" endeavor to hook up a Service Worker, we called navigator.serviceWorker.register() to register our Service Worker, merely we didn't do anything with the returned value. It turns out that annals() returns a Hope for a "registration."

In that location are a bunch of nifty things we tin can do with a registration, plus a handful of useful things we tin can practice with the navigator.serviceWorker ServiceWorkerContainer object that I haven't mentioned yet.

A few things you lot can do with a registration:

  • Call update() on the registration to manually refresh the Service Worker script. The Service Worker script automatically attempts to refresh when nosotros call navigator.serviceWorker.register(), simply we might desire to poll for updates more than frequently than that, e.1000. once an hour or something.
  • Telephone call unregister() on the registration to terminate and unregister the Service Worker.
  • Call navigator.serviceWorker.getRegistration() to become a Promise for the existing registration (which may be null).
  • Get a reference to the agile ServiceWorker object (the ServiceWorker object that controls the current folio) via the navigator.serviceWorker.controller holding or the registration'due south .active property.
  • We can also use a registration to go a reference to a .waiting Service Worker or an .installing Service Worker.
  • One time we have a ServiceWorker, nosotros tin can call postMessage() to transport information to the Service Worker. Within the Service Worker script, nosotros can heed for a message MessageEvent, which has a .information property containing the message object.

Finally, the actual implementation

We get-go by adding code to the Service Worker'due south script to listen for a posted bulletin:

          addEventListener('message', messageEvent => {
if (messageEvent.data === 'skipWaiting') return skipWaiting();
});

Next, we'll demand to listen for a .waiting ServiceWorker, which requires some inconvenient boilerplate code. The registration fires an updatefound event when in that location's a new .installing ServiceWorker; the .installing ServiceWorker fires a statechange event once it's in the "installed" waiting land.

Here's the boilerplate. Note that since this code runs in the web page, and not in the Service Worker script, nosotros've written it in ES5 JavaScript.

          office listenForWaitingServiceWorker(reg, callback) {
function awaitStateChange() {
reg.installing.addEventListener('statechange', role() {
if (this.state === 'installed') callback(reg);
});
}
if (!reg) render;
if (reg.waiting) render callback(reg);
if (reg.installing) awaitStateChange();
reg.addEventListener('updatefound', awaitStateChange);
}

We'll listen for a waiting Service Worker, then prompt the user to refresh. When the user requests a refresh, nosotros'll mail a message to the waiting Service Worker, asking it to skipWaiting(), which will fire the controllerchange event, which we've configured to refresh the page.

          // reload in one case when the new Service Worker starts activating
var refreshing;
navigator.serviceWorker.addEventListener('controllerchange',
role() {
if (refreshing) return;
refreshing = true;
window.location.reload();
}
);
office promptUserToRefresh(reg) {
// this is just an example
// don't utilize window.ostend in real life; it's terrible
if (window.confirm("New version available! OK to refresh?")) {
reg.waiting.postMessage('skipWaiting');
}
}
listenForWaitingServiceWorker(reg, promptUserToRefresh);

Note that this Arroyo #iii works even if there are multiple open tabs. All of the tabs will display the "update available" prompt; when the user clicks the "refresh" link in whatsoever one of them, the new Service Worker will take control of all of the tabs, causing all of them to refresh to the new version.

However, Arroyo #iii has 2 major drawbacks.

First, this approach is hella complicated. Maybe you can re-create and paste this average, but you won't be able to debug it unless you truly grok information technology, which requires an intimate agreement of the Registration API, the Service Worker life bike, and how to exam for the problems resulting from code/information inconsistency.

Despite the complexity, Arroyo #3 seems to exist the standard recommended approach. Google has put together a Udacity course on Service Workers, and this is the arroyo that they certificate. (It occupies a meaning fraction of the course, and includes 3 sub-quizzes building on one another. 😭)

I wish this approach were easier; there are a couple of tickets on the Github repository where these things are managed. Specifically, I wish it were easier to listen for a waiting Service Worker, and I wish it were possible to make a Service Worker skip waiting without posting a message to information technology.

Imagine if this whole section were replaced past a snippet like this:

          office promptUserToRefresh() {
// don't use confirm in production; this is but an case
if (confirm('Refresh now?')) reg.waiting.skipWaiting();
}
if (reg.waiting) promptUserToRefresh();
reg.addEventListener('statechange', function(e) {
if (due east.target.state === 'installed') {
promptUserToRefresh();
} else if (east.target.state === 'activated') {
window.location.reload();
}
});

Second, this approach does not and cannot fix the browser's built-in Refresh button. To refresh the page, users have to use an in-app Refresh link instead.

When the user actually has multiple tabs open, that might be for the best. Nosotros need the user'due south browser to refresh all of our app's tabs at in one case in gild to restart the "app" and ensure data consistency. But when there'south just ane tab remaining, we can do better.

Approach #4: Skip waiting when the last tab refreshes with the Clients API (buggy in Firefox)

The Request object has a .manner property. Information technology'south mostly used for managing the security restrictions around the same-origin policy and Cross-Origin Resources Sharing (CORS) with settings similar: same-origin, cors, and no-cors. But it has one more style that'southward not similar the others: navigate.

When a Service Worker'south fetch listener sees a request in navigate mode, and so nosotros know that a new document is being loaded. At that point, we can count the currently running tabs ("clients") with the Clients API.

Oh yes, my friends, information technology's time to learn yet another new API.

Using the global clients Clients object, we can get a list of Client objects. Each Client has an id property, corresponding to each FetchEvent'southward .clientId property.

Only we don't really demand to do anything with the individual clients or their IDs; we just need to count them. From at that place, we tin can use the registration object in the global scope of the Service Worker to run into if there'south a Service Worker waiting, and mail service it a message asking information technology to skip waiting in that case.

          if (upshot.request.mode === 'navigate' && registration.waiting) {
if ((wait clients.matchAll()).length < 2) {
registration.waiting.postMessage("skipWaiting");
}
}

There are two bug with this snippet. Start, as of November 2017, this technique works nifty in Chrome 62, just it doesn't work in Firefox. registration.waiting is always cipher from inside the Service Worker. Hopefully the Firefox team volition sort this out before long.

There'southward an another, deeper trouble. The navigation has already started, and the old v1 Service Worker is already treatment information technology. If the v1 Service Worker handles the request in the normal fashion with caches.match(), information technology will respond to the navigation request with a response from the one-time v1 Service Worker'south enshroud (considering caches.match() ever prefers to friction match from the oldest bachelor cache), but the refreshed page will then endeavour to load all of its scripts and styles from the new v2 cache, blatantly violating lawmaking consistency.

In this case, nosotros'll return a blank response that instantly refreshes the page, instead. We'll utilise the HTTP Refresh header to refresh the page after 0 seconds; the page will appear bare for an instant, then refresh itself as a v2 tab.

          addEventListener('fetch', issue => {
result.respondWith((async () => {
if (event.request.mode === "navigate" &&
effect.asking.method === "GET" &&
registration.waiting &&
(look clients.matchAll()).length < 2
) {
registration.waiting.postMessage('skipWaiting');
return new Response("", {headers: {"Refresh": "0"}});
}
return await caches.match(event.request) ||
fetch(effect.asking);
})());
});

With this code in place, the browser'south Refresh button does what it's "supposed" to practice when we're using just 1 tab.

I recommend combining this technique with the Approach #three in-app "refresh" link described earlier, to handle the case where multiple tabs demand to be updated, and to handle Firefox's buggy implementation of registration.waiting.

Whoa! You Know Service Workers Now!

Typical Service Worker tutorials like the "Using Service Workers" guide on MDN introduce only the raw basics (equally of November 2017): a uncomplicated install listener that caches a list of URLs in a versioned Enshroud, a fetch listener, and an activate listener to elapse obsolete Caches.

But as you've read this article and the previous article, you lot've learned near almost all of the other classes in the ServiceWorker API:

  • Cache
  • CacheStorage (caches)
  • Client
  • Clients (clients)
  • ExtendableEvent
  • FetchEvent
  • InstallEvent
  • Navigator.serviceWorker (the ServiceWorkerContainer)
  • ServiceWorker (especially its postMessage() method)
  • ServiceWorkerGlobalScope
  • ServiceWorkerRegistration

As of 2017, there are other features associated with Service Workers (Background Sync and Push Notifications), but those APIs are a piece of cake compared to the nightmare you've been through to set up the Refresh button.

So, congratulations on getting through all of this material! You're pretty much a Service Worker wizard at this bespeak.

Discuss on Hacker News
Discuss on Reddit

source: Pop Team Epic (also known as Poptepipic) (ポプテピピック, Poputepipikku)

P.South. Redfin is hiring .

Source: https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68

Posted by: lopezhithatides88.blogspot.com

0 Response to "How To Debug An Unknown Error Occurred When Fetching The Script Service Worker"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel