Today’s topic is the update of the core Service Worker in PWA. This is a problem that is easily overlooked by developers because most of them are probably not familiar with it.
Due to its two characteristics of asynchronous installation and continuous running, Service workers must be updated with great care. Because it has the ability to intercept and process network requests, the version of the web page (mainly the requests sent) must be consistent with that of the Service Worker, otherwise the new version of the Service Worker will process the old version of the web page. Or a web page with two versions of Service Worker control causing problems.
Over the past two years, PWA has become so popular in the WEB community that you’ve probably heard of it, if you haven’t used it. Service Worker (hereinafter referred to as SW) is the most complex and core part of PWA. There are mainly Caches API (Caches. Put, Caches. AddAll, etc.), Service Worker API (self.addeventListener, Self. skipWaiting, etc.) and Registration API (reg.installing, reg.onupdatefound, etc.).
This article no longer popularizes the basic of SW, I mainly want to talk about SW update here. It’s not easy to get the SW and the page in full sync. Until then, I assume you already know:
- The role of the SW
- How to register for SW (
navigator.serviceWorker.register
) - Install -> waiting -> activate -> fetch
Two no-nos to organizing SW
Before we start talking about updates to the SW in a serious way, it’s worth identifying two taboos when organizing SW. There are two methods to avoid when applying SW to your site:
Do not give service-worker.js a different name
For static files, the popular practice is to give them a unique name at each build based on the content (or random factors such as the time of day), such as index.[hash].js. Because these files are infrequently modified, a long mandatory cache can greatly reduce the time it takes to access them.
Unfortunately, this is not appropriate for SW. Let’s say we have a project
-
The first page of index.html contains a
-
The service-worker.v1.js caches index.html for speed or offline availability.
-
After some update, index.html now needs to be used with service-worker.v2.js, so the
-
However, we found that when users visited the site, due to the effect of the old version of service-worker.v1.js, the index. HTML retrieved from the cache still referenced v1, not v2 after we upgraded it.
This happens because upgrading v1 to v2 relies on the index. HTML reference address changing, which is itself cached. Once this dilemma is reached, there is nothing we can do about it unless the user manually clears the cache and unloads V1.
soservice-worker.js
Use the same name and do not add anything to the file name that will change.
Do not cache service-worker.js
The reason is similar to the first point, and is to prevent the browser from requesting a new version of THE SW because of cache interference. After all, we can’t ask the user to clear the cache. Therefore, it is safer to set cache-control: no-store to SW and related JS (such as sw-register.js, if separate).
SW waiting state
SW is registered by the navigator. ServiceWorker. Register (swUrl, options) method. However, unlike normal JS code, this execution appears to the browser in two different ways:
-
If you don’t already have an active SW, install it and activate it.
-
If an existing SW is installed, make a request to the new swUrl to get the content and compare it to the existing SW. If no, end the installation. If not, install the new version of SW (install phase) and leave it waiting (waiting phase).
At this point, there will be two SWS on the current page, but they are in different states, as shown below:
- If the old SW controls all pagesAll closed, the old SW ends running and the new SW is activated (execute)
activated
Phase) to make it take over the page.
This is a mild and safe approach, equivalent to a natural phase-out of the old and new versions. After all, closing all pages is a user’s choice, not a programmer’s. One other thing to note is that because of the browser’s internal implementation, when a page switches or refreshes itself, the browser waits until the new page is rendered before destroying the old one. This means that there is a crossover time between the old and new pages, so simply switching pages or refreshing won’t cause the SW to update, the old SW will still take over the page and the new SW will still be waiting. (This also requires that in addition to onUpdatefound, we also need to determine whether there is a waiting SW, i.e. Reg. waiting. But that is beyond the scope of this article.)
Suppose we provide a major upgrade and want a new SW to take over the page as soon as possible, what should we do?
Method 1: skipWaiting
In an emergency, it is easy to think of “jumping the queue” to solve the problem, which is used in real life for special vehicles such as ambulances and fire trucks. SW also provides the programmer with the possibility of implementing such a scheme in a method called self.skipwaiting () inside SW.
self.addEventListener('install', event => {
self.skipWaiting()
// Precache other static content
})
Copy the code
This allows the new SW to “jump the queue”, forcing it to immediately take over control of all pages from the old SW, while the old SW is simply “cut down”. Lavas initially used this solution because it was too easy to think of and too easy to implement, and the temptation was overwhelming.
Unfortunately, there are pitfalls. Let’s imagine the following scenario:
-
Sw.v1.js has been installed on a page named index.html (the actual address is sw.js, just to distinguish the expression)
-
The user opens the page, all network requests go through sw.v1.js, and the page is loaded.
-
Because the SW asynchronous installation features, general in the browser is idle, he’ll go to perform the words of the navigator. ServiceWorker. Register. The browser notices that sw.v2.js exists, installs it and tells it to wait.
-
But because sw.v2.js had self.skipwaiting () during the install phase, the browser forced sw.v1 to retire and instead let sw.v2 activate and control the page immediately.
-
The user’s subsequent operations in the index.html, if any, are handled by sw.v2.js.
Obviously, the first half of the same page is controlled by sw.v1.js, and the second half is controlled by sw.v2.js. The inconsistency between the two can easily lead to problems and even crashes. For example, sw.v1.js precaches a v1/image.png, and when sw.v2.js is activated, the old version of the precache is usually removed and a cache such as v2/image.png is added. Therefore, if the user has a poor network environment or is down from the network, or if a cache policy such as CacheFirst is used, the browser finds that v1/image.png is no longer in the cache. Even if the network environment is normal, the browser has to make a second request to retrieve the already cached resource, wasting time and bandwidth. Moreover, such SW-induced errors are difficult to reproduce and DEBUG, adding instability to the program.
You can only use this solution if you can ensure that the same page works in two versions of the SOFTWARE.
Method 2: skipWaiting + refresh
The problem with method 1 is that after skipWaiting a page is controlled by two SWS in succession. Since the new SW is installed, the old SW is obsolete, and therefore pages processed with the old SW are also obsolete. What we need to do is let the new SW handle the page from start to finish, so that it is consistent and meets our needs. So we thought of refreshing and discarding pages that had already been processed.
You can listen to the ControllerChange event where the SW is registered (not in the SW) to see if the SW controlling the current page has changed as follows:
navigator.serviceWorker.addEventListener('controllerchange', () = > {window.location.reload();
})
Copy the code
When you find that the SW that controls you has changed, refresh yourself and allow yourself to be controlled by the new SW from beginning to end to ensure data consistency. That’s true, but a sudden update interrupts the user and can cause discomfort. The source of the refresh is the SW change; The SW change was due to a skipWaiting for the browser to install the new SW, so most of the refresh will happen within a few seconds after the page loads. Users just start to browse content or fill in information encounter inexplicable refresh, may hit the keyboard.
There are two other points to note:
SW updates and page refreshes
When talking about the waiting state of the SW, I once said that the SW cannot be updated simply by switching the page or refreshing the page, but here again involves the SW update and the page refreshing, which inevitably leads to confusion.
Let’s take a look at the logic, which is not complicated:
-
The refresh does not update the SW, that is, the old SW will not exit and the new SW will not be activated.
-
The method is to force the SW to alternate between old and new using skipWaiting. After the alternation is complete, controllerChange listens for the change and performs a refresh.
So the cause and effect of the two are opposite, not contradictory.
Avoid unlimited refresh
When using Chrome Dev Tools’s Update on Reload feature, using the code above causes an infinite self-refresh. To make up for this, we need to add a flag, as follows:
let refreshing = false
navigator.serviceWorker.addEventListener('controllerchange', () = > {if (refreshing) {
return
}
refreshing = true;
window.location.reload();
});
Copy the code
Method three: Give the user a hint
The second method has an idea worth learning, that is, “events are triggered by SW changes and refresh is performed in event listening”. But refreshing the page without warning is unacceptable, so wouldn’t it be nice to give the user a prompt to click and update the SW and trigger a refresh?
The general process is as follows:
-
When the browser detects the presence of a new (different) SW, it installs it and lets it wait, firing the Updatefound event
-
We listen for events and pop up a prompt asking the user if they want to update the SW
-
If the user confirms, a message is sent to the waiting SW asking it to execute skipWaiting and take control
-
Because the SW change triggers the ControllerChange event, we refresh the page in the callback of this event
What’s worth noting here is step 3. Because the response code for the user click is in the normal JS code, and the skipWaiting call is in the SW code, the two need a postMessage to communicate.
On the code side, let’s take a step-by-step look at the Lavas implementation:
Step 1 is done by the browser and has nothing to do with us. The second step requires us to listen for the Updatefound event, which needs to be listened by the Registration object returned when the SW is registered. Therefore, we can usually listen directly during the Registration, so that we do not need to obtain the object later, which will be complicated.
function emitUpdate() {
var event = document.createEvent('Event');
event.initEvent('sw.update'.true.true);
window.dispatchEvent(event);
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js').then(function (reg) {
if (reg.waiting) {
emitUpdate();
return;
}
reg.onupdatefound = function () {
var installingWorker = reg.installing;
installingWorker.onstatechange = function () {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
emitUpdate();
}
break; }}; }; }).catch(function(e) {
console.error('Error during service worker registration:', e);
});
}
Copy the code
Here we notify the outside world by sending an event (named sw.update, which is inside the emitUpdate() method), because the prompt is a separate component and is inconvenient to present directly here. Of course, if your application has a different structure, you can modify it yourself. Display a prompt or simply use Confirm to ask the user to confirm.
Step 3 deals with user clicks and communicates with the SW. The code to deal with the click is relatively simple, so it will not be repeated. Here is mainly listed the communication code with SW:
try {
navigator.serviceWorker.getRegistration().then(reg= > {
reg.waiting.postMessage('skipWaiting');
});
} catch (e) {
window.location.reload();
}
Copy the code
Note that messages are sent to the waiting SW through reg.waiting, not to the current old SW. The SW part is responsible for receiving messages and performing queue-jumping logic.
// service-worker.js
// SW will no longer execute skipWaiting during install
self.addEventListener('message', event => {
if (event.data === 'skipWaiting') { self.skipWaiting(); }})Copy the code
Step 4 is the same as method 2. It also uses navigator.serviceWorker to listen for the Controllerchange event to perform the refresh operation, so the code is not listed again.
Disadvantages of method three
From the running results, this method gives consideration to both fast update and user experience, which is the best solution at present. But it also has disadvantages.
Drawback 1: too complicated
-
In terms of number of files, there are at least 2 files involved (registering SW, listening for Updatefound and handling DOM rendering and clicking in normal JS, listening for messages and performing skipWaiting is in SW code), not to mention that we may split the modules for the code. Split the DOM presentation click and the SW registration into two files
-
In terms of API types, there are Registration apis (Registration, used when listening to updatefound and sending messages), SW lifecycle and APIS (skipWaiting), and plain OLD DOM apis
-
The testing and debugging methods are complex. You need to create an environment for at least two versions of SW and master the debugging methods.
In particular, in order to achieve the SW “jump the queue” after the user clicks, it needs to respond from DOM click, send a message to SW, and then operate in SW. This sequence of operations spans several JS and is very unintuitive and complex. For this reason, Google boss Jake Archibald has proposed to the W3C to simplify the process and allow queue-jumping in normal JS via reg.waiting. SkipWaiting (), rather than just inside the SW.
Drawback 2: the update must be done through JS
Updates to the SW can only be done by the user clicking the button on the notification bar, using JS, and not by the browser’s refresh button. This is really a browser design problem, not a solution problem.
On the other hand, if the browser did this for us, it would allow the refresh of one Tab to force the refresh of other tabs, which is not safe or understandable given the current tab-based browser.
The only possible optimization is when there is only one Tab on the sw-controlled page, refreshing the Tab will save us a lot of operations if it can update the SW, and will not cause cross control problems. But this may add to the browser’s judgment costs and lose the aesthetic value of consistency, which is probably a distant dream too.
Afterword.
SW is very powerful, but it also involves a relatively large number of apis, and is a powerful technology (rocket Science in foreign articles) that requires considerable learning costs. Updates to SW are important for sites that use SW, but as mentioned above, the solution is also relatively complex, far exceeding the complexity of other common front-end infrastructure technologies (such as DOM apis, JS operations, closures, etc.). However, SW has only been developing for two or three years since its inception. We believe that with the W3C’s continuous revision and the continued use of front-end circles, there will be a cleaner, more automatic, more complete solution, and we may be able to use SW as easily as we use DOM API.
Refer to the article
-
Two improvements to Service Worker updates – source of writing this article
-
The Service Worker Lifecycle – A popular article about Service workers from Google Developers
-
How to Fix the Refresh Button When Using Service Workers – the fourth method is mentioned, but there are still compatibility issues in Firefox