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:

  1. Offline caching
  2. Being pushed
  3. 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 workerupdate

How does the service worker update when we change the cache policy

  1. service workerfileURLThe update of
  2. service workerThe file content is changed
  3. The user is performing no operation24Hours can be updated automatically

To change theservice workertheURL

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 workerThe 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


  1. A pageindex.htmlIt’s already installedold_sw
  2. The user opens the page and all network requests passold_swThe page is loaded
  3. becauseservice workerIt 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
  4. But because thenew_swininstallPhase hasself.skipWaiting()“, so the browser forced exitold_swAnd letnew_swActivate and control the page immediately
  5. If the user is inindex.htmlIf there is a network request for subsequent operations, thenew_swProcessing is obviously the same page that the first half is made up ofold_swControl, the latter part bynew_swControl. 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 tohtmlcaching

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 likeNative AppAlso, 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 WorkerListening 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.

usepostMessageSend 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