preface

What is a Service Worker? Service Worker is an independent script running in the background of the browser. It enables single-page applications to intercept Web requests, cache offline, notify offline, and process complex business logic concurrently. It can be used to build progressive Web applications (PWA). This article will use several demos to familiarize yourself with how Service workers work, how Service workers send messages and cache paths in web pages.

How Service workers work

This article refers to the chapter of Service Worker in this article, which is very detailed about Service Worker, registration installation process and working principle. The demo in this article is also built and expanded after reading the introduction of this article. I hope you can finish the introduction of Service Worker in this article before running the demo. Here is a brief summary of the knowledge points needed for Service Worker development

scope

Service Worker scope is a URL path, namely the registration to the navigator. ServiceWorker. Register method URL, such as: The navigator. ServiceWorker. Register (‘. / a/b/sw. Js’) for the scope of the https://somehost/a/b/, The Service Worker controls all pages in the https://somehost/a/b/ directory and can include the following:

  • https://somehost/a/b/index.html
  • https://somehost/a/b/c/index.html
  • https://somehost/a/b/anothor.html
  • .

Due to the SPA project is only one index. The HTML entrance, so generally use the root directory of the sw files to control the whole application cache, which USES the navigator. ServiceWorker. Register (‘. / sw. Js’) to register the service worker, Its scope is “/ ‘.

The life cycle

Every time you open an application, the main process needs to register the service worker (hereinafter referred to as SW). After successful registration, sw will be installed and Install Event will be triggered. Generally, application cache will be processed in this Event. After the activation, the SW enters the Idle state. All requests on the status page trigger Fetch events until the application is shut down. The diagram below:

It is important to note that a sw scope is a url (spa application scope are generally set to the root directory, the current domain name, discussed later sw, we are all the default scope for root directory), so the web is under the same domain name * * public a sw, and used in the domain of web pages are closed, sw will be close to an end. ** One browser can open multiple web pages with the same domain name at the same time, so the relationship between SW and web pages is one-to-many, as shown below:

The above will cause a problem, because all pages with the same domain name share a sw, only when all pages under the SW scope are closed will the SW end, so when there is a sw update how to do, we use examples to analyze the SW installation registration activation process

The test code

<! DOCTYPE html> <head> <title>Service Worker Demo</title> </head> <body> <button id="loadImage">load</button> <img id="img" alt="demo image" style="width: 100px; height: 100px;" /> <img SRC ="./imgs/test.jpg" Alt ="demo image" /> <script> if ('serviceWorker' in navigator) {// because 127.0.0.1:8000 Is the host of all test demos // To prevent scope contamination, Will be installed before the cancellation of an effective Service of all the Worker. The navigator serviceWorker. GetRegistrations (). Then (regs = > {for (let reg of regs) { Reg. The unregister ()} the navigator. ServiceWorker. Register ('. / sw. Js')})} / * * * to test img fetch * / document.getElementById('loadImage').onclick = function () { if (!! document.getElementById('img').src.includes("test2.jpg")) { document.getElementById('img').src = "" } else { console.log('load ./imgs/test2.jpg') document.getElementById('img').src = "./imgs/test2.jpg" } } </script> </body> </html>Copy the code
// sw.js console.log('service worker registered successfully ') self.addEventListener('install', () => {// install callback logic handle console.log('service worker installed successfully ')}) self.addeventListener ('activate', () => {console.log('service worker activated successfully ')}) self.addeventListener ('fetch', ${event.request. Url} (clientId:${event.clientid}) ')})Copy the code

Register with sw for the first time

The first time you execute the above example, the console will print:

Service worker registration succeeds service worker installation succeeds service worker activation succeedsCopy the code

It can be seen that the process of registration → installation → activation will be executed during the first registration, but the FETCH event is not triggered, indicating that the Service Worker who successfully registers for the first time fails to intercept the page request before registration

It should be noted that the first registration here means that there is no registered SW in the webpage of this domain name (the domain name under the sw scope), and it is the first time to open the webpage containing the registered SW code. If you have registered before and want to reproduce the situation of the first registration, you can open the application →Service Workers to manually cancel the registration of sw under the domain name, and then close the webpage under the domain name, and then open the webpage for the first time to reproduce the scene of the first registration of SW

Refresh the page

If you refresh the page that has been registered with the SW, the console console displays the following result:

Service worker grabs request success: < http://127.0.0.1:8000/imgs/test.jpg > (d87d3b7 clientId: 2-8606-4897-8 c0b36 a1d - 8772929)Copy the code

As you can see, rerefreshing the page does not perform the registration, installation, and activation process, but blocks all current page requests

Open a new TAB

Open a new http://127.0.0.1:8000 TAB in your browser, and the console console displays the following result:

Service worker successfully registered service worker successfully installed service worker successfully activated service worker successfully fetching request :<http://127.0.0.1:8000/> (clientId:) Service worker grabs request success: < http://127.0.0.1:8000/imgs/test.jpg > (d87d3b7 clientId: 2-8606-4897-8 c0b36 a1d - 8772929) service Worker grabs the request is successful: < http://127.0.0.1:8000/ > (clientId:) service worker fetching request success: < http://127.0.0.1:8000/imgs/test.jpg > (clientId:2eff2973-793e-406b-b749-0ec789412ce6)Copy the code

If you open another TAB, the console displays:

Service worker successfully registered service worker successfully installed service worker successfully activated service worker successfully fetching request :<http://127.0.0.1:8000/> (clientId:) Service worker grabs request success: < http://127.0.0.1:8000/imgs/test.jpg > (d87d3b7 clientId: 2-8606-4897-8 c0b36 a1d - 8772929) service Worker grabs the request is successful: < http://127.0.0.1:8000/ > (clientId:) service worker fetching request success: < http://127.0.0.1:8000/imgs/test.jpg > Service worker :<http://127.0.0.1:8000/> (clientId:2eff2973-793e-406b-b749-0ec789412ce6) service worker :<http://127.0.0.1:8000/> (clientId: service Worker grabs the request is successful: < http://127.0.0.1:8000/imgs/test.jpg > (clientId: c9e bdb9d ec3a - 394-4-8 d3e - aa61bacfb7af)Copy the code

Now that you have opened three tabs of the same domain name, you will notice that the other two tabs print the same results except for the one that has been refreshed

Shows that the new web page will also perform registration to install, activate the process, and it can intercept all sw page requests (including the current page and other pages with domain name before the request), which may explain the sw and web terminals when a one-to-many relationship, before open the sw page every time synchronization, intercept all requests, of course, these requests are clientId, You can distinguish whether it is triggered by the current page

The end of the sw

At this point, close all the pages under the domain name, and then reopen (need to wait about 20 seconds (chrome test data) to open again, Because of the sw will have a short waiting time) before the end of / < http://127.0.0.1:8000/ > (< http://127.0.0.1:8000/ >) will find that will print as follows:

Service worker registration succeeds service worker installation succeeds service worker activation succeedsCopy the code

After all web pages under the SW scope are closed, the SW will end, and the process of registration, installation, and activation will be performed again when the next page is reopened

Tip: Open a new TAB after closing all pages under the domain name, and then check in the console to see if the SW is really closed

Click to view all registrations. If http://127.0.0.1:8000/ buttons change to Unregister and Start, sw is finished. If http://127.0.0.1:8000/ buttons change to Stop, Inspect and Unregister, SW is running

Sw has ended 🔼

Sw is still running 🔼

Update the sw

Try updating the SW file to modify the setup success callback print and fetch event callback print as follows:

Service worker grabs the request is successful: < http://127.0.0.1:8000/imgs/test.jpg > (clientId: c9d4c78a - fd9 - b5b5 ca02-4-5 dedbbbc04a7) service Worker registration succeeds. Installation succeeds. 1Copy the code

It can be found that the sw is refreshed after updating, and the SW is re-registered and installed, but no activation is performed. Then click the load button in the page and you will get the following print, printing “grab request succeeded” instead of “grab request succeeded 1”, indicating that the current execution is still the old SW code

Service worker grabs the request is successful: < http://127.0.0.1:8000/imgs/test2.jpg > (clientId: c9d4c78a - fd9 - b5b5 ca02-4-5 dedbbbc04a7)Copy the code

End the sw process (see the “End sw” section above), reopen a new page, and click the load button in the page or refresh the page to find the print update, indicating that the SW has been updated

Service worker grabs request 1 success: < http://127.0.0.1:8000/imgs/test2.jpg > (clientId: 2 dffb96a - ff2e - 42 ce - 9 da3 27 dd7940c114)Copy the code

From this, it can be concluded that after the sw update, the new SW will only be re-registered and installed before the end of the current SW, not immediately. The latest SW code will only be executed when the sw process is finished and the next time it is reopened

Execute the latest sw immediately (skipWaiting)

If waiting for the software to finish every update will result in our latest software code not running immediately, is there a way to make the software work immediately? Sure, executing self.skipWaiting() in the install callback will skip the waiting process and make the new software work immediately

ininstallExecute in the callbackself.skipWaiting()Skip the wait and update the next callback print as follows:

Refresh the web page and find the following result

Service worker grabs request 1 success: < http://127.0.0.1:8000/imgs/test.jpg > (clientId: 00 f75 c3be85-4-442 e - ecca22a a5e7-73594) service Worker registered successfully sw-lifecycle-test.js:10 Service worker installed successfully 2 sw-lifecycle-test.js:16 Service worker activated successfullyCopy the code

Click the load button in the next page to find the print update, indicating that the SW immediately takes effect

Service worker grabs the request is successful 2: < http://127.0.0.1:8000/imgs/test2.jpg > (clientId: 00 f75 c3be85-4-442 - e - ecca22a a5e7-73594)Copy the code

Final flow chart

A flowchart for opening a web page with sw code is as follows:

Application Service Worker

Send the message

The SW and client are one-to-many. Therefore, the SW can receive messages from all clients and distinguish the client from the other client based on their IDS. Then, the client can only receive messages sent by the SW to itself.

Main process (Web Client)

The client send the message to sw: use the navigator. ServiceWorker. Controller. PostMessage API

let count1 = 100
document.getElementById('send').onclick = function () {
  navigator.serviceWorker.controller.postMessage(++count1)
}
Copy the code

The client receives messages from the SW

The navigator. ServiceWorker. AddEventListener (' message ', function (e) {the console. The log (' receives the Service Worker news: ', e.d ata); })Copy the code

service worker

The SW sends a message to the client

Method 1: Send the message in the message callback, using the event.source.postMessage method will send the message back to the corresponding client(id is event.source.id).

Self. addEventListener('message', event => {console.log('Service Worker(receive message):', event. Event) if (event.source && event.source.postMessage) {// Send a message to client console.log('Service 'with id event.source.id Worker(send message):', event.data, event.source.id) event.source.postMessage(' I received a message ')}})Copy the code

Method 2: Use self.client. get to obtain the specified client and use postMessage to send a message to the client

/** * Send a message to client * @param {Number} clientId * @param {String} message * @returns */ function sendMessageToClient(clientId, message) { // Get the client. const client = await self.clients.get(clientId); if (! client) return; // Send a message to the client. client.postMessage(message); }Copy the code

MatchAll = self.clients.matchall = self.clients.matchall = self.clients.matchall = self.clients.matchall = self.clients.matchAll = self.clients.matchAll = self.clients.matchAll = self.clients.matchAll = self.clients.matchAll

@param {String} message * @returns */ function sendMessageToAllClients(message) { self.clients.matchAll().then(function(clients) { clients.forEach(function(client) { client.postMessage(message); }); })}Copy the code

Test code:

<! DOCTYPE html> <head> <title>Service Worker Message Demo</title> </head> <body> <button id="send">send</button> <button id="loadImage">load</button> <img id="img" alt="demo image" style="width: 100px; height: 100px;" /> <img src="./imgs/test.jpg" alt="demo image" style="width: 100px; height: 100px;" /> <script> if ('serviceWorker' in navigator) {// Since 127.0.0.1:8000 is the host for all test demos // to prevent scope contamination, Will be installed before the cancellation of an effective Service of all the Worker. The navigator serviceWorker. GetRegistrations (). Then (regs = > {for (let reg of regs) { Reg. The unregister ()} the navigator. ServiceWorker. Register ('. / sw. Js')})} / * * * to test img fetch * / document.getElementById('loadImage').onclick = function () { if (!! document.getElementById('img').src.includes("test2.jpg")) { document.getElementById('img').src = "" } else { Console. log('load./imgs/test2.jpg') document.getelementById ('img').src = "./imgs/test2.jpg"}} /** * Listen for message events */ The navigator. ServiceWorker. AddEventListener (' message ', function (e) {the console. The log (' main process (receives the message) : ', e.d ata); }) /** * send message */ let count1 = 100 document.getelementByid ('send').onclick = function () {console.log(' main process (send message) ', + + count1) navigator. ServiceWorker. Controller. PostMessage (+ + count1)} / serviceWorker * * * * / Ready event navigator.serviceWorker.ready.then( registration => { registration.active.postMessage("Hi service worker"); }); </script> </body> </html>Copy the code
// sw.js console.log('service worker registered successfully ') self.addEventListener('install', () => {console.log('service worker installed successfully ') // skipWaiting self.skipwaiting ()}) self.addeventlistener ('activate', () => {console.log('service worker activated successfully ')}) let count = 0; self.addEventListener('fetch', ${event.request. Url} (clientId:${event.clientid}) ') ++count; Event.waituntil (async function() {await sendMessageToClient(event.clientid, 'Fetch request succeeded: ${event.request.url}`, `${count}`) }()) }) self.addEventListener('message', Event => {console.log('Service Worker :', Event.data) if (event.source && event.source.postMessage) {// Send a message to client console.log('Service 'with id event.source.id Worker(send message):', event.data, event.source.id) event.source.postMessage(' I received the message ') sendMessageToClient(event.source.id, SendMessageToAllClients (' this message is sent to all clients ')}}) /** * Send a message to client * @param {Number} clientId * @param {String} message * @returns */ async function sendMessageToClient(clientId, message) { // Get the client. const client = await self.clients.get(clientId); if (! client) return; // Send a message to the client. client.postMessage(message); } /** * Send a message to all clients * @param {String} message * @returns */ function sendMessageToAllClients(message) { self.clients.matchAll().then(function(clients) { clients.forEach(function(client) { client.postMessage(message); }); })}Copy the code

The cache

We can use the cache API to cache paths (such as images, files, request urls, etc.). Follow the instructions of the service worker to configure the path.

  1. Initialize cache: initialize the cache path in the sw install callback, that is, the default cache path is added to the cache through the cache API. Do not cache the root directory, otherwise the page will be retrieved from the cache

    const CACHE_NAME = 'my-site-cache-v1'; Const ROOT_URL = '< http://127.0.0.1:8000/ >'; Const urlsToCache = [// '/', // do not cache the root directory, otherwise pages will always load cache cannot update '/imgs/test.jpg', // default cache /imgs/test.jpg]; /** ** skipWaiting can skip the wait. */ self.addeventListener ('install', (event) = > {/ / install the callback logic to handle the console. The log (' < = = = = = = service worker install success = = = = = = > ') / / skip waiting for self skipWaiting () / * * * Initialize the cache in install. Cache the path in urlsToCache */ event.waitUntil(new Promise((resolve, Reject) => {// Return Promise caches that handle things related to cache updates. Open (CACHE_NAME). Then (async function(cache) {let currentCaches = await Cache.keys () const rootCache = currentCaches. Find (c => c.url === ROOT_URL) if (rootCache) {// If the root directory of the cache, Delete (rootCache)} // path in cache urlsToCache await cache.addAll(urlsToCache) currentCaches = await Cache.keys () console.log(' cache initialized successfully ', currentCaches) resolve(' cache initialized successfully ')})}))})Copy the code
  2. When the page requests data from the cache, the cache is returned

    */ self.addEventListener('fetch', event => {wsLog(' service worker fetched the request successfully: ${event.request.url}`) event.respondWith( caches.match(event.request) .then(function(response) { if (response) { // Log (' Hit cache, return cached value ', response) return response; } else {console.log(' cache not found, normal request ', event.request) return fetch(event.request); }})); })Copy the code

    If the request is not cached and you want to add it to the cache immediately, you can also dynamically add the cache in the “fetch” callback, but be careful not to cache the root directory, otherwise you will always get the cached page when requesting the page and the application will not be able to update

    */ self.addEventListener('fetch', event => {wsLog(' service worker fetched the request successfully: ${event.request.url} ') /** * Returns the cache for requests cached during the install phase; For uncached requests, */ event.respondwith (caches. Match (event.request).then(function(response) {if (response) {// Match cache, Return the cached value to console.log(' hit cache, return cached value ', response) return response; } else {if (event.request.url === ROOT_URL) {// Do not cache the root directory, otherwise the page will always load the cache and cannot update console.log(' Request is the root directory, ', event.request) return fetch(event.request); } // No cache hit, add this request to cache console.log(' No cache hit, cache this request ', event.request) /** * Clone request * The request is a stream and can only be used once. */ const fetchRequest = event.request.clone(); */ const fetchRequest = event.request.clone(); Return fetch(fetchRequest). Then (function(response) {/** * Check whether the response is valid * ensure that the response is valid * Check that the state of the response is 200 * Ensure The type of response is basic, which means that the request is cognate, which means that requests from third parties cannot be cached. */ if(! response || response.status ! == 200 || response.type ! == 'basic') { return response; } /** * Clone response * Response is a Stream, so its body can only be used once * So in order for the browser and cache to use the body, we must clone the body to the browser. */ const responseToCache = response.clone(); Console. log(' add cache ', event.request) caches. Open (CACHE_NAME). Then (function(cache) {cache.put(event.request, responseToCache); }); return response; }); }})); })Copy the code

Cache example code:

<! DOCTYPE html> <head> <title>Service Worker Demo</title> </head> <body> <button id="loadImage">load</button> <img id="img" alt="demo image" style="width: 100px; height: 100px;" /> <img SRC ="./imgs/test.jpg" Alt ="demo image" /> <script> if ('serviceWorker' in navigator) {// because 127.0.0.1:8000 Is the host of all test demos // To prevent scope contamination, Will be installed before the cancellation of an effective Service of all the Worker. The navigator serviceWorker. GetRegistrations (). Then (regs = > {for (let reg of regs) { Reg. The unregister ()} the navigator. ServiceWorker. Register ('. / sw. Js')})} / * * * * / img cache used for testing purposes document.getElementById('loadImage').onclick = function () { if (!! document.getElementById('img').src.includes("test2.jpg")) { document.getElementById('img').src = "" } else { console.log('load ./imgs/test2.jpg') document.getElementById('img').src = "./imgs/test2.jpg" } } </script> </body> </html>Copy the code
// sw.js /** * cacheApi: <https://developer.mozilla.org/zh-CN/docs/Web/API/Cache> */ const CACHE_NAME = 'my-site-cache-v2'; Const ROOT_URL = '< http://127.0.0.1:8000/ >'; Const urlsToCache = [// '/', // do not cache the root directory, otherwise pages will always load cache cannot update '/imgs/test.jpg', // default cache /imgs/test.jpg]; /** ** skipWaiting can skip the wait. */ self.addeventListener ('install', (event) = > {/ / install the callback logic to handle the console. The log (' < = = = = = = service worker install success = = = = = = > ') / / skip waiting for self skipWaiting () / * * * Initialize the cache in install. Cache the path in urlsToCache */ event.waitUntil(new Promise((resolve, Reject) => {// Return Promise caches that handle things related to cache updates. Open (CACHE_NAME). Then (async function(cache) {let currentCaches = await Cache.keys () const rootCache = currentCaches. Find (c => c.url === ROOT_URL) if (rootCache) {// If the root directory of the cache, Delete (rootCache)} // path in cache urlsToCache await cache.addAll(urlsToCache) currentCaches = await Cache.keys () console.log('Cache initialized successfully ', CurrentCaches) resolve('Cache initialized successfully ')})}) /** * sw activate callback */ self.addeventListener ('activate', (event) = > {/ / activate the callback logic to handle the console. The log (' < = = = = = = service worker activation success = = = = = = > ')}) / * * * to intercept request, */ self.addEventListener('fetch', event => {console.log(' service worker fetched the request successfully: ${event.request.url}`) if (! Event.request) return /** * Cache policy * If 0, cache is returned for requests cached during install; For uncached requests, normal go online requests * if it is 1, for requests cached in install phase, return to cache; */ const mode = 1 if (mode === 0) {/** * For requests cached by install phase, return to cache; */ event.respondwith (caches. Match (event.request). Then (function(response) {if (response) {// Match cache, WsLog (' hit cache, return cached value ', response) return response; } else {wsLog(' cache not found, normal request ', event.request) return fetch(event.request); }})); } else {/** * Return cache for requests cached in the Install phase; For uncached requests, */ event.respondwith (caches. Match (event.request).then(function(response) {if (response) {// Match cache, Return the cached value to console.log(' hit cache, return cached value ', response) return response; } else {if (event.request.url === ROOT_URL) {// Do not cache the root directory, otherwise the page will always load the cache and cannot update console.log(' Request is the root directory, ', event.request) return fetch(event.request); } // No cache hit, add this request to cache console.log(' No cache hit, cache this request ', event.request) /** * Clone request * The request is a stream and can only be used once. */ const fetchRequest = event.request.clone(); */ const fetchRequest = event.request.clone(); Return fetch(fetchRequest). Then (function(response) {/** * Check whether the response is valid * ensure that the response is valid * Check that the state of the response is 200 * Ensure The type of response is basic, which means that the request is cognate, which means that requests from third parties cannot be cached. */ if(! response || response.status ! == 200 || response.type ! == 'basic') { return response; } /** * Clone response * Response is a Stream, so its body can only be used once * So in order for the browser and cache to use the body, we must clone the body to the browser. */ const responseToCache = response.clone(); Console. log(' add cache ', event.request) caches. Open (CACHE_NAME). Then (function(cache) {cache.put(event.request, responseToCache); }); return response; }); }})); }})Copy the code

By executing the above code, you can see that images/test.jpg has been cached through the console application → cache

Click the load button in the page to load images/test2.jpg. If you change mode to 1 in the code, the cache policy is “for requests cached in the install phase, return to cache; For uncached requests, add to the cache (except the root path) and images/test2.jpg will be added to the cache as well

debugging

See here for debugging methods.

All demo code

service worker demo

reference

Service Worker MDN

The Service Worker practice