Service workers are often asked about in interviews, but there is no complete article online
Introduction to the
As early as In May 2014, W3C proposed an HTML5 API like Service Worker, which is mainly used for persistent offline caching. You can only do one thing at a time in a single main thread of js running in a browser. If a piece of code is too time consuming, it will always be tied up in the browser’s main thread, resulting in performance degradation. Based on this problem, the W3C proposed web workers, which give tasks that take too long to complete to the Web Worker and then tell the main thread via post Message, which gets the result via onMessage. However, Web workers are temporary, and the results of each run cannot be maintained for a long time. The next time there is a complex operation, it needs to be recalculated. In order to solve this problem, Service Worker is launched, which increases the offline cache capacity compared with Web Worker. Service Worker mainly has the following features and functions:
- Offline caching
- Being pushed
- Request to intercept
How to useService Worker
registered
Create an HTML and add the register service Worker at the bottom
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', {scope: '/'})
.then((a)= > {
console.log('Service Worker registration successful')
})
.catch((err) = > {
console.log('Service worker registration failed')})})}Copy the code
This code first determines whether the service Worker is supported and then invokes its register method. The scope official title here describes the subdirectory of the content that the Service Worker wants to control, which feels confusing. For example, my directory is like this
scope
./config
service-worker
config
fetch
cache.addAll
/
index.html
service worker
service worker
service worker
promise
The installation
After registering the service Worker, the service Worker will install and trigger the install event, in which some resources can be cached as follows:
// Listen for the install event of the service worker
this.addEventListener('install', (event) => {
// If the service worker has been installed successfully
// The event.waitUtil callback is called
event
.waitUntil(
// Call CacheStorage cache after successful installation, with caches.open() before use
// Open the corresponding cache space
caches.open('my-test-cache-v1')
.then((cache) = > {
// Add it through the addAll method of the cache object
return cache.addAll([
'/'.'/index.html'])}})))Copy the code
First, we listen for the Install event and call waitUntil, which is used to extend the life of the event by internally passing in a Promise and waiting until the Promise internally changes to resolve. This is mainly to extend the service worker’s installing cycle and reach the Installed life cycle after the resource cache is completed. You can view the Cache information in the Cache Storage of the Application
Service Worker
Request to intercept
Service Worker has the function of request interception. When the page sends AN HTTP request, the Service Worker can intercept the request through the fetch event and give its own response. Therefore, for security, IT needs to use HTTPS.
this.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) = > {
// If the service worker has its own put back, it returns directly, saving one HTTP request
if (response) {
return response;
}
// If the service worker does not return, request the real remote service directly
var request = event.request.clone();
return fetch(request)
.then((res) = > {
// The request fails
if(! res || res.status ! = =200) {
return res;
}
// If the request is successful, the request is cached
var responseClone = res.clone;
caches
.open('my-test-cache-v1')
.then((cache) = > {
cache.put(event.request, respondClone)
})
returnres; }})})))Copy the code
Listen for the FETCH event and then call event.respondWith, which works like waitUntil, returning the response to the browser when the Promise resolved is passed in. Compares the data in the cache to see if there is any cached content. If so, use the cached content. If not, request the remote service. Since the Service Worker intercepts all requests, it is important to determine what needs to be cached and what does not. For example, Ajax does not need to be cached. When we revisit index. HTML, we can see that the index. HTML is fetched directly from the service worker
service worker
update
How does the service worker update when we change the cache policy
service worker
fileURL
The update ofservice worker
The file content is changed- The user is performing no operation
24
Hours can be updated automatically
To change theservice worker
theURL
If we cache index.html in sw1.js for the first time and change the content of index.html, we find that the page content is not changed, then we need to change the service worker. We can choose to change sw1.js to sw2.js, which means re-register a service worker to cache the new file as follows:
service worker
url
index.html
sw1.js
index.html
v1
v2
html
To change theservice worker
The file content
If the content of sw.js is updated, when visiting the website page, the browser gets the new file and compares the /sw.js file byte by byte, it will think that there is an update, so it will install the new file and trigger the install file, but the old service worker that is already active is still running. The new service worker enters a waiting state after installation. The new service worker does not take effect in the next reopened page until all opened pages are closed and the old service worker is automatically updated. For example, add a version number to sw.js for service worker updates
var version = '0.0.1';
// Skip the wait and go directly to active
this.addEventListener('install', funciton (event) {
event.waitUntil(self.skipWaiting())
})
this.addEventListener('activate'.function (event) {
event.waitUntil(
Promise.all([
// Update the client
self.clients.claim(),
// Clean up the old version
caches.keys().then((cacheList) = > {
return Promise.all(
cacheList.map((cacheName) = > {
if(cacheName ! = ='my-test-cache-v1') {
return caches.delete(cacheName)
}
})
)
})
])
)
})
Copy the code
First, we call sell. skipWaiting to skip the installing stage and take over the old service worker. If you don’t perform this step, you’ll see
service worker
service worker
skipWaiting
installing
service worker
activate
- A page
index.html
It’s already installedold_sw
- The user opens the page and all network requests pass
old_sw
The page is loaded - because
service worker
It has the feature of asynchronous installation, usually when the browser is idle, it will execute that sentencenavigator.serviceWorker.register
. That’s when the browser notices there’s anew_sw
“So the installation kept him waiting - But because the
new_sw
ininstall
Phase hasself.skipWaiting()
“, so the browser forced exitold_sw
And letnew_sw
Activate and control the page immediately - If the user is in
index.html
If there is a network request for subsequent operations, thenew_sw
Processing is obviously the same page that the first half is made up ofold_sw
Control, the latter part bynew_sw
Control. This can lead to inconsistent behavior and unknown errors
Manual updateservice worker
As above, update with the same version number
var version = '1.0.1'
navigator.serviceWorker.register('/sw.js')
.then((reg) = > {
if(localStorage.getItem('sw_version') !== version) {
reg.update()
.then((a)= > {
localStorage.setItem('sw_version', version)
})
}
})
Copy the code
Automatic updates
In addition to being triggered by the browser for updates, the Service Worker also applies a special caching policy: if the file has not been updated for 24 hours, the Update will force an Update when triggered. This means that in the worst case, the Service Worker will be updated once a day.
How tohtml
caching
As mentioned in the example above for updates to the service worker, resources can be cached in the cache database. One problem with the above example is that we say the resource is cached, but the page display is the changed content when we need a second access. Because HTML does not have a file summary like other static resources, special processing is required for HTML files. Since it is still the old resource that is fetched from the cache for the first time, I have a few thoughts on this issue: Is the HTML files for special treatment, if there is a net is to get a new resource from the server, HTML file cache cache strategy generally use negotiation, r if no network using a cache of HTML content, so it can achieve every page of every visit to is the latest, then also achieved the effect of offline. Fetch is used to determine whether there is a web and whether the fetch is in HTML format:
this.addEventListener('fetch'.function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
var isHtml = /\.html/.test(reponse ? response.url : ' ');
var onLine = navigator.onLine;
// If there is no network, use the cached content
if(! onLine) {return response;
}
// If there is a web and it is not HTML, and response exists, return response
if(! isHtml && response) {return response;
}
// ...
})
Copy the code
This ensures that the HTML is up to date every time the web is available.
Being pushed
Message push has a wide range of application scenarios:
- When the new product is put on the shelves, push the message to the user and click it to enter the product details page
- Users have not been on the site for a long time, and push notification of updates to the site during this time using push notification allows our application to look like
Native App
Also, improve the user experience
To obtain authorization
Before subscribing to the message, you need to obtain the user’s authorization to use the message push. The specific code is as follows:
navigator.serviceWorker.register('./sw.js')
.then((reg) = > {
res.pushManager.getSubscription().then((subscription) = > {
// If the user does not subscribe
if(! subscription) { subscribeUser(reg) }else {
console.log('you have subscribed our notification')}})})Copy the code
If you have subscribed, the following popover will not pop up again, and if not subscribeUser will be called
Subscription push service
As the message source, the server entrusts the push service to send the message to the browser that subscribes to the message, so the server needs to keep the unique identity of the browser. Web-push is used to generate a public key and a private key, and the public key generates a unique identifier for the browser through the service worker, which is handed to the server. The server uses this unique identifier to push messages to the browser.
function subscribeUser(registration) {
const applicationServerPublicKey = 'BKzIIoV8RgBqSlOZ5GMle3OY6rZoB-aaoRxldWN8jn5MZOXbtH5tFTchxDRW1jTSLTCOdNPfyk4Yszx0Lk1Clts';
const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
/ / subscribe
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey,
})
.then((subscription) = > {
$.post({
type: 'post'.url: 'http://localhost:3000/add'.data: {
subscription: JSON.stringify(subscription)
},
success: (res) = > {
console.log(res)
}
})
})
.catch((err) = > {
console.log('Failed to subscribe the user: ', err)
})
}
Copy the code
ApplicationServerPublicKey is just the public key is generated by the web – a push, and then call pushManager. Subscribe to generate a unique identifier of the browser to the backend. This subscription is in the following format
{"endpoint":"https://fcm.googleapis.com/fcm/send/eAWELgsiTME:APA91bGZ4UwYtr26b0JE8K4sTNNFN8Z8GJ07QgDZHJP9aAqeMsjqiJJaaXd4Ype62vm5v4E jRnD0MuSD5ouBLYy6aT6nU5tWFpp5DjSjPmt_bh-h2Nm5pLo9-xY8H83Q8MHTynY7onKk"."expirationTime":null,"keys": {"p256dh":"BLpOkRk1lLRXG8kMP3Yc4D6SUmz3aagln-ysP0lslwJsPA7SQhkmeytSFRCLZKBToBwMe3qRaUAMcJ0R3B1ZND4"."auth":"pWaweBbyQqi5lNDR0Rqqew"}}
Copy the code
Once this information is available, the server can use this identity to push messages to the specified browser
The server implements push
Web-push message push is also needed here. Since message push requires The FCM service of Google, FCM service cannot be used due to the network of our own network. Therefore, Google Browser can not be used here. After looking up a lot of information, I found that Firefox can achieve the same effect. Therefore, IT is suggested to use Firefox to achieve the same effect, and there will be no wall problem.
const webpush = require('web-push');
// Push data
const payload = {
title: 'A New article'.body: 'Click on it.'.icon: 'xx'.// Image link
data: {
url: 'www.baidu.com'}}const vapidKeys = {
publicKey: 'BKzIIoV8RgBqSlOZ5GMle3OY6rZoB-aaoRxldWN8jn5MZOXbtH5tFTchxDRW1jTSLTCOdNPfyk4Yszx0Lk1Clts'.privateKey: 'm5rk4Cann9l5pp7TiLPuNmL2Ho_zmIvgM3wz07EZSSs'
}
const pushSubScription = {
"endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABeaki7zwcdJ8r-2PZhwjyeCkHN3GaFAI4NQP8awz3e5svu0xDP6Peanq7iNTRd 6S8weseu8JGpJDmLF1V2CcSZRExeWfLt0p5ksuNvCQmYnC4Bwy6wBzUGt-yQRAQMdq9_RKsEnadYfWAQt6LHENfaUr0gKcJJcj1Jb6vGfel-eqjEmjE"."keys": {
"auth": "QyYLx2m29E-3a5kXzqdIDg"."p256dh": "BEX1qgwC7MIRw-Vck7wsQPw5M8CIhkQ6thqs5ZwmPkXYy1zF-7sXvKE9hxeZtlm1rHd5lpvpjJf3q26rJje8zUc"
}
}
webpush
.sendNotification(pushSubScription, JSON.stringify(payload), {
vapidDetails: {
subject: 'mailto:[email protected]'. vapidKeys, }, }) .then((res) = > {
console.log(res);
})
.catch((err) = > {
console.log(err)
})
Copy the code
Get the subscription from the previous client and the publickKey and privateKey generated earlier, then call webpush. SendNotification to actively push the message
service Worker
Listening to thepush
After the server pushes the information to the client, we need to listen for the push event and show the effect
self.addEventListener('push'.function (event) {
console.log('push');
// var notificationData = event.data.json();
// var title = notificationData.title;
const title = 'push works';
const options = {
body: 'push is working'.icon: 'resource/logo.png'.badge: 'resource/logo.png'
}
event.waitUntil(self.registration.showNotification(title, options));
})
this.addEventListener('notificationclick'.function(event) {
event.notification.close();
event.waitUntil(
clients.openWindow('https://baidu.com'))})Copy the code
Here we listen for push and get the content of the active message pushed by the server, just to keep things simple
notificationclick
The page
The service worker cannot manipulate the DOM directly, but it can communicate with the Web page through the postMessage method and have the page manipulate the DOM.
usepostMessage
Send the request
Service worker sends data: Sends a message to the page in sw.js using client.postMessage. Example code:
this.clients.matchAll()
.then(function (clients){
if (clients && clients.length) {
clients.forEach((client) = > {
// Send data
client.postMessage('sw update')})}})Copy the code
Page to send data: use the navigator in the main page. ServiceWorker. Controller. PostMessage () to send the data
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('home update')}Copy the code
Receive data
Receive messages from the home page in the service worker as shown in the following example:
self.addEventListener('message'.function (event) {
console.log(event.data); // home update
});
Copy the code
Accept the message from the service worker on the main page as shown in the following example:
navigator.serviceWorker.addEventListener('message'.function (event) {
console.log(event.data)
});
Copy the code
The resources
PWA documentation understanding-service-worker-scope Handle service worker updates carefully add push notifications to web applications