Following This Discussion, Where there is a comment that speaks of
by overriding self.fetch, self.XMLHttpRequest, and self
There's a few things to cover here:
Multiple Service Workers in a Single Scope
There can only be one active service worker for a given scope. If you attempt to register two different service worker scripts that each have the same scope, the second registration will trigger the service worker update flow:
// There's an implied default scope of '/'.
// See https://stackoverflow.com/a/33881341/385997
navigator.serviceWorker.register('/sw1.js');
// If called later on, this will trigger the update flow.
// You'll only end up with one of the two being active.
navigator.serviceWorker.register('/sw2.js');
The exact timing for when sw2.js
will activate and take control over any existing clients depends on whether you're using self.skipWaiting()
and self.clients.claim()
inside of sw2.js
. Once sw2.js
activates, sw1.js
will be marked as redundant.
Another way of asking what I think is the same question is whether you can have multiple service workers controlling the same client page at the same time. The answer is no, you can have at most one service worker controlling any client page, and only that service worker will be able to respond to fetch
events originating from the page.
Using importScripts to Share Common Handlers
Instead of attempting to register multiple service workers with the same scope, using importScripts() to pull in logic that's defined in a different JavaScript file sounds like a reasonable approach. There are a few things to keep in mind when using importScripts()
in this fashion:
importScripts()
needs to be called during the initial startup execution of your service worker code, not inside an event handler. I.e. "lazy-loading" of importScripts()
is not supported.importScripts()
executes all the of the code inside of the file(s) synchronously, one by one, in the order in which they're listed. You can have multiple importScripts()
, or importScripts()
inside of files that are themselves imported, and they'll all execute in a defined order.self
will be set to the same ServiceWorkerGlobalScope that would be used if the code were in the top-level service worker. I.e., there's no difference between calling self.addEventListener()
inside of an imported script or inside of the top-level service worker.importScripts()
will be cached by default, using the same mechanism that's built in to the browser for caching your top-level service worker file. While there are some changes to the service worker specification underway to change this, as of right now, those cached importScripts()
files will be used indefinitely as long as their filenames don't change. So a best practice is to either include a version number or a hash in the file names of anything referenced with importScripts()
.Multiple fetch Event Handlers
What happens when you have multiple calls to self.addEventListener('fetch')
?
From the previous section we know that it's not relevant whether those multiple calls originate inside of an importScripts()
resource or the top-level service worker. They both operate on the same global scope.
The behavior is well-defined: when a client page makes a request, it will trigger the fetch
handlers of the controlling service worker one by one, in the order in which they were registered, until the first call is made to event.respondWith()
. One one fetch
event handler calls respondWith()
, no other fetch event handlers will be triggered, and it's the sole responsibility of that handler to (eventually) return a Response
to the client page.
Since the order in which your self.addEventlistener('fetch')
calls matter, make sure that you list the files in your importScripts()
in an appropriate order, and either include your call to importScripts()
before or after you define any fetch
event handlers in your top-level service worker, depending on which you want to take precedence.
While you can use conditional logic to determine whether or not to call event.respondWith()
, that logic can't be asynchronous, since the service worker won't wait to see whether event.respondWith()
is called. It needs to synchronously move on to the next event
handler (assuming there is one).
So inside a fetch
handler, you can use conditional logic like
// This can be executed synchronously.
if (event.request.url.endsWith('.html')) {
event.respondWith(...);
}
but you can't use conditional logic like:
// caches.match() is asynchronous, and the service worker will have
// moved on to the next `fetch` handler before it completes.
caches.match('index.html').then(response => {
if (response) {
event.respondWith(...);
}
});
There's a live code sample that you can explore if you want to see the multiple-handlers behavior for yourself.