Service Workers essentially act as a proxy server between the Web application and the browser, and can also act as a proxy between the browser and the network when the network is available. They are designed, among other things, to enable the creation of an effective offline experience, intercept network requests and take appropriate action based on whether the network is available and whether updated resources reside on the server. They also allow access to push notifications and background synchronization apis. (From: Link)

To put it simply, the ServiceWorker (SW) runs in the background of a page, and pages that use SW can use sw to intercept requests made by the page, along with CacheAPI to cache requests locally to customers

So we can:

  • The files on the page are stored on the client, so that no resource request is sent to the server when the page is opened next time, which greatly accelerates the page loading speed
  • When you open the page offline, you can make a request in the SW to update the local resource file
  • Implement offline access to the page

But there are also some problems

  • When a page is opened, no page request is sent to the server. Therefore, when a new version of the page is available on the server, the client cannot upgrade in time
  • There are certain compatibility issues with the SW

IE, PC compatibility is not good, mobile android support is good, ios to 12+. But considering that SW will not affect the normal operation of the page, so the project can still be put into production.

The basic example

index.html


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Sw demo</title>

</head>
<body>
    
</body>
<script src="index.js"></script>
<script>
    if(navigator.serviceWorker){
        navigator.serviceWorker.register('sw.js').then(function(reg){
            if(reg.installing){
                console.log('client-installing');
            }else if(reg.active){
                console.log('client-active')}}}</script>
</html>
Copy the code

index.js

document.body.innerHTML="hello world!";
Copy the code

sw.js

var cacheStorageKey = 'one';

var cacheList = [
    "index.html"."index.js"
]
self.addEventListener('install'.function (e) {
    console.log('sw-install');
    self.skipWaiting();
})
self.addEventListener('activate'.function (e) {
    console.log('sw-activate');
    caches.open(cacheStorageKey).then(function (cache) {
        cache.addAll(cacheList)
    })
    var cacheDeletePromises = caches.keys().then(cacheNames= > {
        return Promise.all(cacheNames.map(name= > {
            if(name ! == cacheStorageKey) {// if delete cache,we should post a message to client which can trigger a callback function
                console.log('caches.delete', caches.delete);
                var deletePromise = caches.delete(name);
                send_message_to_all_clients({ onUpdate: true })
                return deletePromise;
            } else {
                return Promise.resolve(); }})); }); e.waitUntil(Promise.all([cacheDeletePromises]
        ).then((a)= > {
            return self.clients.claim()
        })
    )
})
self.addEventListener('fetch'.function (e) {
    e.respondWith(
        caches.match(e.request).then(function (response) {
            if(response ! =null) {
                console.log(`fetch:${e.request.url} from cache`);
                return response
            } else {
                console.log(`fetch:${e.request.url} from http`);
                return fetch(e.request.url)
            }
        })
    )
})
Copy the code

instructions

This completes a simple SW page, now through the server to access the page HTML, JS resources will be directly read from the client local, realize the page quickly open and offline access

  • Both the client and the SW have different event callbacks, which will be triggered in different sw lifecycles, more on this later
  • When the page is opened for the first time, the sw will execute the install callback, execute self.skipWaiting() and then execute activate, which will cache the files in the cache list
  • CacheStorageKey is a cache identifier. When the value of the cacheStorageKey changes, sw Activate deletes the cache and calls cache.addAll again to set the cache
  • The SW FETCH event intercepts the request from the page and will be handled differently depending on the cache

Life cycle and events

The lifecycle of a SW application is simply abstracted into three categories

  • The installation: When the page is opened for the first time, the corresponding SW file is loaded
  • activity: After loading the SW file, open the page
  • update: Open the page if the sw file on the server is inconsistent with that on the client

The client

The name of the installing active
The installation The trigger Don’t trigger
activity Don’t trigger The trigger
update Don’t trigger The trigger

sw

The name of the install activate fetch
The installation The trigger The trigger Don’t trigger
activity Don’t trigger Don’t trigger The trigger
update The trigger The trigger Don’t trigger

To sum up:

  • The client is active except for installing when it is first opened
  • Only fetch is executed in the active state of the SW side, and only Install and Activate are executed in the installation and update state

The page communicates with the SW

In terms of communication, I have translated articles before. Please refer to the link address if you are interested. Here I directly show the encapsulated communication interface interface

With the communication interface, we can optimize things like notifying the page to respond to the customer when the cacheStorageKey changes

The client

function send_message_to_sw(msg){
    return new Promise(function(resolve, reject){
        // Create a Message Channel
        var msg_chan = new MessageChannel();
        // Handler for recieving message reply from service worker
        msg_chan.port1.onmessage = function(event){
            if(event.data.error){
                reject(event.data.error);
            }else{ resolve(event.data); }};// Send message to service worker along with port for reply
        navigator.serviceWorker.controller.postMessage(msg, [msg_chan.port2]);
    });
}
Copy the code

sw

function send_message_to_client(client, msg){
  return new Promise(function(resolve, reject){
      var msg_chan = new MessageChannel();
      msg_chan.port1.onmessage = function(event){
          if(event.data.error){
              reject(event.data.error);
          }else{ resolve(event.data); }}; client.postMessage(msg, [msg_chan.port2]); }); }function send_message_to_all_clients(msg){
  clients.matchAll().then(clients= > {
      clients.forEach(client= > {
          send_message_to_client(client, msg).then(m= > console.log("SW Received Message: "+m)); })})}Copy the code

Cache resource files dynamically

The above method requires that the cacheList be written in advance, and there is a certain amount of maintenance. Now here is a method that does not require maintenance of the cacheList:

self.addEventListener('fetch'.function (e) {
  e.respondWith(
    caches.match(e.request).then(res= > {
      return res ||
        fetch(e.request)
          .then(res= > {
            const resClone = res.clone();
            caches.open(cacheStorageKey).then(cache= > {
              cache.put(e.request, resClone);
            })
            returnres; }})})));Copy the code

The fetch event determines whether there is a cache or not. If there is no cache, the fetch event will issue the corresponding request and cache it

The disadvantage of this approach is that the page cannot be static the first time it loads because the SW’s FETCH event will not be triggered by the SW’s install declaration cycle.

Page URL with parameters

For some cases where page rendering results are related to URL parameters, the above architecture cannot meet the corresponding localization requirements. The previous approach was to add the address of the entry page to the cacheList, which could not accommodate urls with dynamic parameters.

Cache requests dynamically within fetch

This practice is described in the dynamic cache resource file section and will not be repeated.

Notify sw cache entry page using communication interface

The client

navigator.serviceWorker.register(file).then(function (reg) {
    if (reg.installing) {
        //send_message_to_sw({pageUrl:location.href})
    }
    if (reg.active) {
        send_message_to_sw({pageUrl:location.href})
    }
    return reg;
})
Copy the code

sw

self.addEventListener('message'.function(e){
  var data=e.data;
  if(data.pageUrl){
    addPage(data.pageUrl)
  }
})
function addPage(page){
  caches.open(cacheStorageKey).then(function(cache){ cache.add(page); })}Copy the code

The active client sends a message to the SW, and the SW can obtain the corresponding page URL for caching.

Note: the installing event on the client cannot use the message interface. You can send a message to the client in the ACTIVATE event of the SW to request the URL of the current page

Q&A

The sw file must be at least in the same directory as the entry page file, for example:

  • /sw.js can manage /index.html
  • /js/sw.js does not manage /index.html

I have been in a pit here for a long time…

webpack-sw-plugin

Introduce a sw plugin for webpack, which is very convenient for sw pages, github address

The installation

npm install --save-dev webpack-sw-plugin
Copy the code

Webpack configuration

const WebpackSWPlugin = require('webpack-sw-plugin');
module.exports = {
    // entry
    // output
    plugins:[
        new WebpackSWPlugin()
    ]
}
Copy the code

Client Configuration

import worker from 'webpack-sw-plugin/lib/worker';
worker.register({
    onUpdate:(a)= >{
        console.log('client has a new version. page will refresh in 5s.... ');
        setTimeout(function(){
            window.location.reload();
        },5000)}});Copy the code

The effect

  • A system that automatically generates pages to interact with the SW without providing additional SW files
  • Automatic url adaptation with parameters
  • When the webPack output file changes, the client’s onUpdate will be triggered. In the example above, when the output file changes, the client will refresh after 5 seconds and use the new file