Some days ago when I was free, I roughly learned about Service Worker. I reviewed it again recently, and wrote it here after sorting out the content. I hope it will be helpful to you.
At the same time, if there is any mistake or improper description in the article, welcome to help me correct, thank you.
PS: It’s a long article with lots of sample code. Take your time 🙂
introduce
As a relatively new technology, you can think of a Service Worker as a proxy server between a client and a server. There are many things you can do in a Service Worker, such as intercepting client requests, sending messages to the client, making requests to the server, etc. One of the most important functions is offline resource caching.
First, as a new technology, we need to pay attention to its compatibility across browsers. Here’s a picture from Caniuse.com:
For Service workers, those who know Web workers may have a better understanding. Compared with Web workers, it has the same points but also different points.
The same:
- Service workers work in the Worker context and do not have access to DOM, so we cannot obtain DOM nodes from Service workers or operate DOM elements in them.
- We can get through
postMessage
The interface passes data to other JS files; - Code running in the Service Worker does not block, nor does it block code in JS files on other pages;
The difference is that a Service Worker is a process in a browser rather than a thread in the browser kernel, so it can be used in multiple pages after being registered and installed without being destroyed when the page is closed. Therefore, Service workers are well suited for computing complex data that needs to be used on multiple pages — once purchased, the whole family “benefits”.
Another point to note is that for security reasons, Service workers can only be used in HTTPS or local localhost environments.
Registered to install
Let’s use the Service Worker.
If the browser currently in use supports Service workers, the serviceWorker object will exist under window.navigator, and we can use the register method of this object to register a Service Worker.
It should be noted that Service workers have a large number of promises in the process of using them. Students who are not familiar with promises can have a look at relevant documents first. The Service Worker’s registration method also returns a Promise.
// index.js
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('./sw.js', { scope: '/' })
.then(function (reg) {
console.log('success', reg);
})
.catch(function (err) {
console.log('fail', err);
});
}
Copy the code
In this code, we first use if to determine whether the browser supports Service workers and avoid bugs caused by browser incompatibility.
The register method takes two arguments. The first is the path to the service worker file. Note that this file path is relative to Origin, not the directory of the current JS file. The second parameter is the configuration item of the Serivce Worker, which is optional. The more important parameter is the scope attribute. As described in the documentation, it is a subdirectory of the content controlled by the Service Worker. The path represented by this attribute cannot be on the path of the Service Worker file. By default, it is the directory where the Serivce Worker file is located. About this attribute, the document is not very clear, I also have a lot of questions, will be put forward in the following content. I hope the students who know can help me out.
The register method returns a Promise. If registration fails, you can use catch to catch the error message. If the registration is successful, then can be used to obtain a ServiceWorkerRegistration instance, interested students can go through the document.
After registering the Service Worker, the browser automatically installs it for us, so we can listen for its install event in the Service Worker file.
// sw.js
this.addEventListener('install'.function (event) {
console.log('Service Worker install');
});
Copy the code
Similarly, the Service Worker is activated after installation, so we can also listen for activate events.
// sw.js
this.addEventListener('activate'.function (event) {
console.log('Service Worker activate');
});
Copy the code
At this point, we can see our registered Service Worker in the Developer tools of Chorme.
Every 24 hours
Update on reload
Under the same Origin, we can register multiple Service workers. Note, however, that the scope used by these Service workers must be different.
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('./sw/sw.js', { scope: './sw' })
.then(function (reg) {
console.log('success', reg);
})
navigator.serviceWorker.register('./sw2/sw2.js', { scope: './sw2' })
.then(function (reg) {
console.log('success', reg); })}Copy the code
Information and communication
As mentioned earlier, we can use the postMessage method to communicate between Service workers and pages, so let’s try it out.
From pages to Service workers
The first is to send a message from the page to the Serivce Worker.
// index.js
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('./sw.js', { scope: '/' })
.then(function (reg) {
console.log('success', reg);
navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage("this message is from page");
});
}
Copy the code
In order to ensure that the Service Worker can receive the message, we send the message after it is registered. Different from ordinary window.postMessage, in order to send the message to the Service Worker, We will call postMessage method on ServiceWorker instance, here we use is the navigator. ServiceWorker. Controller.
// sw.js
this.addEventListener('message'.function (event) {
console.log(event.data); // this message is from page
});
Copy the code
In the service worker file, we can bind the message event directly to this so that we can receive the message from the page.
For multiple Service workers with different scopes, we can also send information to the specified Service Worker.
// index.js
if ('serviceWorker' in window.navigator) {
navigator.serviceWorker.register('./sw.js', { scope: './sw' })
.then(function (reg) {
console.log('success', reg);
reg.active.postMessage("this message is from page, to sw");
})
navigator.serviceWorker.register('./sw2.js', { scope: './sw2' })
.then(function (reg) {
console.log('success', reg);
reg.active.postMessage("this message is from page, to sw 2");
})
}
// sw.js
this.addEventListener('message'.function (event) {
console.log(event.data); // this message is from page, to sw
});
// sw2.js
this.addEventListener('message'.function (event) {
console.log(event.data); // this message is from page, to sw 2
});
Copy the code
Please note that when we register the Service Worker, if use scope is not Origin, so the navigator. ServiceWorker. The controller will be null. In this case, we can use the postMessage method under reg.active, which activates the Serivce Worker instance after being registered. However, since Service Worker activation is asynchronous, the Service Worker will not be activated immediately when it is registered for the first time. If reg.active is null, the system will report an error. My approach is to return a Promise, poll within the Promise, and resolve if the Service Worker has been activated.
// index.js
navigator.serviceWorker.register('./sw/sw.js')
.then(function (reg) {
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 100)
})
}).then(sw => {
sw.postMessage("this message is from page, to sw");
})
navigator.serviceWorker.register('./sw2/sw2.js')
.then(function (reg) {
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 100)
})
}).then(sw => {
sw.postMessage("this message is from page, to sw2");
})
Copy the code
From Service Worker to page
The next step is to send messages from the Service Worker to the page. Instead of the page sending messages to the Service Worker, we need to call the postMessage method on the WindowClient instance to do this. In the JS file of the page, listen for the message event of navigator.serviceWorker to receive the message.
The easiest way is to get a WindowClient instance from the message sent from the page using event.source, but this method can only send information to the page from which the message came.
// sw.js
this.addEventListener('message'.function (event) {
event.source.postMessage('this message is from sw.js, to page');
});
// index.js
navigator.serviceWorker.addEventListener('message'.function (e) {
console.log(e.data); // this message is from sw.js, to page
});
Copy the code
If you don’t want to be constrained by this, you can use this.clients in the Serivce worker file to fetch additional pages and send messages.
// sw.js
this.clients.matchAll().then(client => {
client[0].postMessage('this message is from sw.js, to page');
})
Copy the code
I have some unresolved questions about this method. In my experiment, the value of scope set when registering a Service Worker will affect the client obtained.
If the scope is set to a non-Origin directory when registering a Service Worker, I cannot obtain the client of the page corresponding to the Origin path in the Service Worker file.
// index.js
navigator.serviceWorker.register('./sw.js', { scope: './sw/' });
// sw.js
this.clients.matchAll().then(client => {
console.log(client); // []
})
Copy the code
I looked for some information, but could not find any explicit documentation about the connection between scope and client. My guess is whether the Service Worker can only obtain the client of the sub-page under the scope path, but I did the react Router test and found that it did not. I hope students who know can help to answer this question. Thank you!
Use Message Channel to communicate
Another good way to communicate is to use Message Channel.
// index.js
navigator.serviceWorker.register('./sw.js', { scope: '/' })
.then(function (reg) {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = e => {
console.log(e.data); // this message is from sw.js, to page
}
reg.active.postMessage("this message is from page, to sw", [messageChannel.por2]);
})
// sw.js
this.addEventListener('message'.function (event) {
console.log(event.data); // this message is from page, to sw
event.ports[0].postMessage('this message is from sw.js, to page');
});
Copy the code
Using this approach allows both ends of the channel to communicate with each other instead of just sending messages to the message source. For example, communication between two Service workers.
// index.jsconst messageChannel = new MessageChannel();
navigator.serviceWorker.register('./sw/sw.js')
.then(function (reg) {
console.log(reg)
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 100)
})
}).then(sw => {
sw.postMessage("this message is from page, to sw", [messageChannel.port1]);
})
navigator.serviceWorker.register('./sw2/sw2.js')
.then(function (reg) {
return new Promise((resolve, reject) => {
const interval = setInterval(function () {
if (reg.active) {
clearInterval(interval);
resolve(reg.active);
}
}, 100)
})
}).then(sw => {
sw.postMessage("this message is from page, to sw2", [messageChannel.port2]);
})
// sw.js
this.addEventListener('message'.function (event) {
console.log(event.data); // this message is from page, to sw
event.ports[0].onmessage = e => {
console.log('sw:', e.data); // sw: this message is from sw2.js
}
event.ports[0].postMessage('this message is from sw.js');
});
// sw2.js
this.addEventListener('message'.function (event) {
console.log(event.data); // this message is from page, to sw2
event.ports[0].onmessage = e => {
console.log('sw2:', e.data); // sw2: this message is from sw.js
}
event.ports[0].postMessage('this message is from sw2.js');
});
Copy the code
First, let the page send information to the two Service workers, and send the port of the information channel. Then use the ports in the two service worker files to set up callback functions to receive messages, and they can send messages to each other and receive messages from across the channel.
Static resource caching
The following is the highlight and the most important feature that a Service Worker can implement — static resource caching.
In normal cases, when a user opens a web page, the browser automatically downloads static resources such as JS files and images. You can check this out using the Network option in Chrome Development Tools.
We can use the Service Worker in conjunction with CacheStroage to cache static resources.
The cache specifies static resources
// sw.js
this.addEventListener('install'.function (event) {
console.log('install');
event.waitUntil(
caches.open('sw_demo').then(function (cache) {
return cache.addAll([
'/style.css'.'/panda.jpg'.'./main.js'])})); });Copy the code
When the Service Worker is installed, we can cache the resources that are specified in the path. The interface name of CacheStroage in the browser is caches. We use the caches. Open method to create or open an existing cache. The cache.addAll method requests linked resources and stores them in the previously opened cache. Since resource downloading and caching are asynchronous behaviors, we need to use the event.waitUntil method provided by the event object, which can ensure that the Service Worker will not be installed until the resource is cached to avoid errors.
You can see the cached resources in the Application Cache Strogae in Chrome Development Tools.
Dynamic caching of static resources
this.addEventListener('fetch'.function (event) {
console.log(event.request.url);
event.respondWith(
caches.match(event.request).then(res => {
return res ||
fetch(event.request)
.then(responese => {
const responeseClone = responese.clone();
caches.open('sw_demo').then(cache => {
cache.put(event.request, responeseClone);
})
returnresponese; }) .catch(err => { console.log(err); }); }})));Copy the code
We need to listen for the FETCH event, which is triggered whenever the user makes a request to the server. Note that the page path cannot be larger than the scope of the Service Worker, otherwise the FETCH event cannot be triggered.
In the callback function we use the respondWith method provided by the event object, which hijacks HTTP requests made by the user and returns a Promise to the user as the result of the response. Then we use the user’s request to match the Cache Stroage. If the match is successful, the resource stored in the Cache is returned. If the match fails, a request is made to the server for resources to be returned to the user and the new resources are stored in the cache using the cache.put method. Since the request and response streams can only be read once, we use the Clone method to store a copy in the cache, while the original is returned to the user
A few things to note here:
- When a user visits a page for the first time, the resource request is made before the Service Worker is installed, so static resources cannot be cached. These resources are cached only when the Service Worker is installed and the user accesses the page a second time.
- Cache Stroage can only Cache static resources, so it can only Cache users’ GET requests;
- The Cache in the Cache Stroage does not expire, but the browser limits its size, so it needs to be cleaned periodically.
For user-initiated POST requests, we can also intercept them and selectively return them by determining the body content carried in the request.
if(event.request.method === 'POST') { event.respondWith( new Promise(resolve => { event.request.json().then(body => { console.log(body); }) resolve(new Response({a: 2})); // Return response}))}}Copy the code
We can judge the properties of the request, such as method and URL, and select different operations in the callback function of the FETCH event.
Cache Stroage is a good choice for caching static resources; For data, we can use IndexedDB to store data. After intercepting user requests, we can use the data cached in IndexDB as a response. I won’t go into details here, but you can find out for yourself.
Update Cache Stroage
As mentioned earlier, when a new service worker file exists, it will be registered and installed, and will not be activated until all pages using the old version have been closed. In this case, we need to clean up our Cache Stroage and delete the old version of the Cache Stroage.
this.addEventListener('install'.function (event) {
console.log('install');
event.waitUntil(
caches.open('sw_demo_v2').then(function(cache) {// Replace cache Stroagereturn cache.addAll([
'/style.css'.'/panda.jpg'.'./main.js'])}}))); const cacheNames = ['sw_demo_v2']; // Cahce Stroage whitelist this.adDeventListener ('activate'.function (event) {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all[keys.map(key => {
if(! cacheNames.includes(key)) { console.log(key);returncaches.delete(key); // Delete Cache Stroage}})]}))});Copy the code
First, when installing a Service Worker, change a Cache Stroage to store it, and then set a whitelist. When the Service Worker is activated, the Cache Stroage that is not in the whitelist will be deleted to release storage space. Again, use event.waituntil to finish deleting the Service Worker before it is activated.
summary
As a new technology, Service Worker has a good application prospect in static resource caching and processing complex data required by multiple pages. As an integral part of implementing PWA, I believe that browser compatibility, functionality diversity, and documentation integrity will continue to improve.
At the same time, there must be a lot of content about Service Worker that I have not learned or talked about, or that I have ignored. Therefore, I hope that I can learn it together with you, especially the property of Scope. I hope that some students who know about it can help me out.
Thanks for reading, please do not reprint without permission 🙂