Advanced JavaScript programming for reading notes 4th Edition (Chapter 27)

Time:2021-1-24

preface

It’s not just notes, but also some supplements

Is JavaScript single threaded?

JavaScript is single threaded, but JavaScript can graft work to independent threads. At the same time, it does not affect the single threaded model (DOM cannot be operated).

Every page opened is equivalent to a sandbox, each page has its own independent content. Worker thread is equivalent to a completely independent secondary sub environment. You cannot interact with the dependent single threaded model API (DOM operation) in a child environment, but you can execute code in parallel with the parent environment.

The difference between worker thread and thread

  1. The underlying implementation principle of worker thread is thread
  2. Worker threads can be concurrent
  3. Shared memory can be realized between worker thread and main thread by using shared array buffer, and concurrency control can be realized by using atomics in JS.
  4. Worker threads do not share all memory
  5. The worker thread and the main thread may not be in the same process
  6. It’s also expensive to create worker threads (worker threads should be used for long-running tasks)

Types of worker threads

  1. Serviceworker service worker thread
  2. Sharedworker shared worker thread
  3. Webworker, worker, dedicated worker thread

WorkerGlobalScope

In the worker thread, there is no window object. The global object is an instance of a subclass of workerglobalscope

  • The dedicated worker thread global object is an instance of the dedicatedworkerglobalscope subclass
  • The shared worker thread global object is an instance of sharedworkerglobalscope
  • A service worker is an instance of serviceworkerglobalscope

Dedicated worker thread worker or webworker

Create a dedicated worker thread

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        //To debug locally, you need to use an absolute path
        const worker = new Worker('./worker.js')
        console.log(worker)
    </script>
</body>
</html>

Security restrictions for dedicated worker threads

The script file of the worker thread can only be the same source as the parent. (but in a worker thread, you can use importscripts to load scripts from other sources)

Using worker objects

The created worker object will not be recycled by the garbage collection mechanism until the worker thread is terminated

  • Onerror, the parent context listens for worker thread errors
  • Onmessage, the parent context listens to messages sent by the worker thread
  • Onmessageerror: the parent context listens to the error caused by the worker thread sending the message (for example, the message cannot be deserialized)
  • The parent context sends a message to the worker thread
  • Terminate() to terminate the worker thread

DedicatedWorkerGlobalScope

The global scope in the worker thread is an instance of the dedicatedworkerglobalscope object, which can be accessed by the self keyword

  • self.postMessage To send a message to the parent context
  • self.close , close the thread
  • self.importScripts , introducing other scripts

Life cycle of dedicated worker threads

The life cycle is divided into initialization, activity and termination. But the parent context is a state that cannot distinguish worker threads. After calling the worker, although the worker object is available, the worker may not be initialized. There may be delays. If close or terminate is not called, the worker thread will always exist, and the garbage collection mechanism will not recycle the worker object. But there are some differences between calling close and terminating. If the web page associated with the worker thread is closed, the worker thread will also be terminated.

  1. Inside the worker thread, if close is called, the worker thread will not end immediately, and it will end after the macro task is completed.
  2. When terminate is called in the parent context, the worker thread ends immediately.
//Dedicated worker thread
self.postMessage('a')
self.close()
self.postMessage('b')

//Parent context
const worker = new Worker('./worker.js')
worker.onmessage = ({ data }) => {
  console.log('data:', data);
}

// consloe
// data: a
// data: b
//Worker thread
self.onmessage = ({data}) => console.log(data);

//Parent context
const worker = new Worker(location.href  + '/worker.js')
//The timer waits for thread initialization to complete
setTimeout(() => {
  worker.postMessage('a')
  worker.terminate()
  worker.postMessage('b')
}, 1000);

// consloe
// a

Create worker thread in line

The dedicated worker thread can be created in the line through the URL of the blob object, without the need for a remote JS file.


const workerStr = `
  self.onmessage = ({data}) => {
    console.log('data:', data);
  }
`;
const workerBlob = new Blob([workerStr]);
const workerBlobUrl = URL.createObjectURL(workerBlob);
const worker = new Worker(workerBlobUrl);

// data: abc
worker.postMessage('abc');

Functions in the parent context can also be passed to the dedicated worker thread and executed in the dedicated worker thread. However, closed variables and global objects cannot be used in functions with parent context.

Const FN = () = > 'function of parent context';
//The FN is converted to a string and executed by itself
const workerStr = `
  self.postMessage(
    (${fn.toString()})()
  )
`
const workerBlob = new Blob([workerStr]);
const workerBlobUrl = URL.createObjectURL(workerBlob);
const worker = new Worker(workerBlobUrl);
worker.onmessage = ({ data }) => {
  //Function of parent context
  console.log(data)
}
const a = 'Hi'
// error, Uncaught ReferenceError: a is not defined
Const FN = () = > '${a}, function of parent context';
const workerStr = `
  self.postMessage(
    (${fn.toString()})()
  )
`
const workerBlob = new Blob([workerStr]);
const workerBlobUrl = URL.createObjectURL(workerBlob);
const worker = new Worker(workerBlobUrl);
worker.onmessage = ({ data }) => {
  //Function of parent context
  console.log(data)
}

Dynamic script execution in worker thread

In the worker thread, you can use importscripts to load and execute scripts. JS loaded by importscripts is executed in order. All imported scripts will share the scope, and importscripts will not be restricted by the same source.

After testing, I use onerror to listen for errors in the parent context, which can catch the errors of non homologous scripts loaded by importscripts, with specific error information.

//Parent context
const worker = new Worker('http://127.0.0.1:8080/worker.js')
window.onerror = (error) => {
  // Uncaught ReferenceError: a is not defined
  console.log(error)
}

//Worker thread
importScripts('http://127.0.0.1:8081/worker.js')

//Worker thread中importScripts加载的脚本
const fn = () => {
  console.log(a)
}
setTimeout(() => fn(), 3000);

Multiple worker threads

Worker threads can also continue to create worker threads. But multiple worker threads bring extra overhead. alsoThe top worker thread, and the child worker thread, must be in the same source as the parent context

Worker thread error

try…… Catch, the error in the thread cannot be caught, but in the parent context, it can be caught using the onerror event

Communication of special worker thread

postMessage

An example has been given in the demo before, so I won’t repeat it here

MessageChannel

The message channel API has two ports. If the parent context needs to communicate with the worker thread, the parent context needs to transfer the port to the worker line

//Parent context
const channel = new MessageChannel()
const worker = new Worker('http://127.0.0.1:8080/worker.js')

//Send port 2 to the worker line
worker.postMessage(null, [channel.port2]);

setTimeout(() => {
  //Sending messages via message channel
  channel.port1 . PostMessage ('I am the parent context ')
}, 2000)
//Worker thread
let channelPort = null

self.onmessage = ({ ports }) => {
  if (!channelPort) {
    channelPort = ports[0]
    self.onmessage = null
    //Monitoring messages through channelport
    channelPort.onmessage = ({ data }) => {
      console.log ('data of parent context: ', data');
    }
  }
}

BroadcastChannel

The same source script can use broadcastchannel for communication. When using broadcastchannel, it must be noted that if the parent context sends a message before the initialization of the worker thread is completed, the worker thread will not receive the message after the initialization. The message does not exist in the message queue.

//Parent context
const channel = new BroadcastChannel('worker')
const worker = new Worker('http://127.0.0.1:8080/worker.js')
//Wait for worker thread initialization to complete
setTimeout(() => {
  channel.postMessage ('message ')
}, 2000)
//Worker thread
const channel = new BroadcastChannel('worker')
channel.onmessage = ({ data }) => {
  console.log(data)
}

Channel Messaging API

Channel messaging API can be used in “document body and iframe”, “between two iframes”, “two documents using shared worker”, or between two “workers”.

Data transmission of special worker thread

Structured cloning algorithm

When using PostMessage to send data, the browser background will copy the data (except the type of symbol). Although the structured cloning algorithm is compatible with the problem of circular reference, it has performance loss for complex objects.

Transferable object

The ownership of the data will be changed. Transferred from parent context to worker thread. Or it can be transferred to the parent context by the worker thread. After the transfer, the data will be erased in the previous context. The second parameter of PostMessage is an optional parameter. It is an array. The data of the array needs to be transferred.

//Parent context
const worker = new Worker('http://127.0.0.1:8080/worker.js')
const buffer = new ArrayBuffer(30)
// 30
console.log ('before sending: ', buffer.byteLength )
//Wait for worker thread initialization to complete
setTimeout(() => {
  worker.postMessage(buffer, [buffer])
  // 0
  console.log ('after sending: ', buffer.byteLength )
}, 2000)
//Worker thread
self.onmessage = ({ data }) => {
  // 30
  console.log ('after worker thread accepts', data.byteLength );
}

On the second parameter of PostMessage

Can be used before worker.postMessage (null, [ channel.port2 ]When sending the channel interface. The parameters of the onmessage event of the worker thread will receive ports, but other data cannot be received. PostMessage should be a special processing of channel data.

SharedArrayBuffer

Shared arraybuffer can be shared in parent context and worker thread. Shared arraybuffer and arraybuffer have the same API, so they can’t be operated directly and need view.

//Parent context
const worker = new Worker('http://127.0.0.1:8080/worker.js')
const sharedBuffer = new SharedArrayBuffer(10)
const view = new Int8Array(sharedBuffer);
view[0] = 1;
// 1
console.log ('before sending: ', view [0]);
worker.postMessage(sharedBuffer)
setTimeout(() => {
  //Print out, 2
  console.log ('after sending: ', view [0]))
}, 2000)
//Worker thread
self.onmessage = ({ data }) => {
  const view = new Int8Array(data);
  view[0] = '2'
}

There is a hidden danger of resource expropriation when parallel threads share resources. You can use atomics to solve this problem. Atomics and shared array buffer can view the notes in Chapter 20.

Thread pool

It costs a lot to start a new worker thread. You can start a fixed number of threads. When the thread is busy, it does not accept new tasks. When the thread is idle, it receives new tasks. These long-term open threads are called thread pools.

The number of threads in the thread pool can refer to the number of threads in the CPU, navigator.hardwareConcurrency Set the number of threads of CPU to the upper limit of thread pool.

The following is the encapsulation in the number. I didn’t find a popular library encapsulation thread pool available in GitHub,https://github.com/andywer/th…It was updated five years ago.

Shared worker thread sharedworker

be careful:

  1. Safari does not support sharedworker
  2. The console in sharedwork may not print in the console of the parent context. (I experimented in chrome. The console in the shared worker thread does not appear in the console of the page.)

Shared worker thread and creation, security restrictions and dedicated worker thread are the same. Shared worker thread can be regarded as an extension of dedicated worker thread.

Sharedworker can be accessed by multiple cognate contexts (cognate web tags). Sharedworker’s message interface and dedicated worker thread are also slightly different.

Sharedworker, there is no way to use the workers in the industryBecause it passed URL.createObjectURL , is the URL inside the browser and cannot be used in other tabs.

Unique identifier of sharedworker

Every time a worker is new, it will return a new worker instance, and sharedworker will only return a new instance without the same token. The identifier of sharedworker can be the path of the worker file and the document source.

//Only one shared worker thread will be instantiated
new SharedWorker('http://127.0.0.1:8080/worker.js')
new SharedWorker('http://127.0.0.1:8080/worker.js')

But if we give sharedworkers of the same source different identities, the browser will task that they are different shared worker threads

//Instantiate two shared worker threads
new SharedWorker('http://127.0.0.1:8080/worker.js', { name: 'a' })
new SharedWorker('http://127.0.0.1:8080/worker.js', { name: 'a' })

In different pages, as long as the logo is the same, the created sharedworker is the same link

Properties of the sharedworker object

  • Onerror listens for errors thrown on shared worker thread objects
  • Port and shared worker thread communication interface, sharedworker will implicitly create, used to communicate with the parent context

The global object in the shared worker thread is the instance of sharedworkerglobalscope, and the properties and methods on the global instance

  • Onconnect, which will be triggered when the shared worker thread establishes a link. The parameter contains the ports array. Port can thread the message back to the parent context. sharedWorker.port.onmessage , sharedWorker.port.start (), will trigger the onconnect event.
  • close
  • importScripts
  • name

Life cycle of shared worker threads

A dedicated worker thread is bound to only one page, while a shared worker thread will not be recycled as long as it has a context link. The shared worker object cannot be closed through terminate because the shared worker thread does not have a terminate method, and the browser is responsible for managing the links of the shared worker thread.

Link sharing worker thread

When the connect event occurs, the sharedworker constructor implicitly creates a message channel and transfers one of the ports to the ports array of the shared worker thread.

However, the startup and shutdown of shared thread and parent context are not symmetrical. Every time a link is opened, the number of ports in the ports array in the connect event is increased by 1, but the page is closed and sharedworker cannot perceive it.

For example, many pages are linked to sharedworker, and now some of them are closed. Sharedworker does not know which pages are closed, so there are ports of closed pages in the ports array, and these dead ports will pollute the ports array.

The method given in the book is to notify sharedworker to clear the dead port in the beforeunload event before the page is destroyed.

Sharedwork example

Example 1

//Parent page
const worker = new SharedWorker('http://127.0.0.1:8080/worker.js')
worker.port.onmessage = ({ data }) => {
    //Print data: 2
    console.log('data:', data);
}
worker.port.postMessage([1, 1]);
//Shared worker thread
const connectedPorts = new Set();
self.onconnect = ({ports}) => {
    if (!connectedPorts.has(ports[0])) {
        connectedPorts.add(ports[0])
        ports[0].onmessage = ({ data }) => {
            ports[0].postMessage(data[0] + data[1])
        }
    }  
};

Example 2

The shared thread generates the ID, marks the interface, and sends it to the page. In the page before unload, the ID is sent to the shared worker thread, and the shared worker thread clears the dead port.

//Parent page 1
const worker = new SharedWorker('http://127.0.0.1:8080/worker.js')
let portId = null
worker.port.onmessage = ({ data }) => {
    if (typeof data === 'string' && data.indexOf('uid:') > -1) {
        //Record the ID of the interface
        portId = data.split(':')[1];
    } else {
        console.log ('number of interfaces: ', data');
    }
}

window.addEventListener('beforeunload', (event) => {
    worker.port.postMessage (` delete: ${PortId} ');
});
//Parent page 2
const worker = new SharedWorker('http://127.0.0.1:8080/worker.js')
let portId = null
worker.port.onmessage = ({ data }) => {
    if (typeof data === 'string' && data.indexOf('uid:') > -1) {
        //Record the ID of the interface
        portId = data.split(':')[1];
    } else {
        console.log ('number of interfaces: ', data');
    }
}

window.addEventListener('beforeunload', (event) => {
    worker.port.postMessage (` delete: ${PortId} ');
});
//Shared worker thread
const uuid = () => {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
      var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
}
//Record the map of the interface
const connectedPortsMap = new Map();

self.onconnect = ({ports}) => {
    if (!connectedPortsMap.has(ports[0])) {
        const uid = uuid();
        connectedPortsMap.set(uid, ports[0])
        //Send the ID of the interface to the page, which is used to delete the interface
        ports[0].postMessage(`uid:${uid}`);
        ports[0].onmessage = ({ data }) => {
            if (typeof data === 'string' &&  data.indexOf ('delete: '> - 1){
                const portId = data.split(':')[1];
                //Delete dead interface
                connectedPortsMap.delete(portId);
            }
        }
    }  
};

setInterval(() => {
    //Number of sending interfaces
    connectedPortsMap.forEach((value) => {
        value.postMessage(connectedPortsMap.size)
    })
}, 3000)

Service worker thread

I feel that the translation of this chapter is a little poor, many words read very awkward, not smooth. And many chapters do not give the example code, many chapters of my hand knock the example code again, put in the article

It is a proxy server thread in the browser, which can intercept requests or cache responses. The page can be used in a non network environment. Similar to the shared worker thread, multiple pages share a service worker thread. In the service worker thread, the service worker thread can use the notifications API, push API and background sync API. In order to use the push API, the service worker thread can continue to wait for pushed events after the browser or tab is closed.

Service worker thread, commonly used in the cache layer of network requests and enabling push notification. Service worker threads can transform the experience of web applications into native applications.

Foundation of notification API

The API used by the browser to display desktop notifications. Here is an example

//Check if notification is allowed
//If direct notification has been allowed
if (Notification.permission === "granted") {
  Let notification = new notification ('sironan '{
    Body: 'sylsa Ronan'
  });
} else if (Notification.permission !== "denied") {
  //If the notification has not been allowed, we ask the user to allow it
  Notification.requestPermission().then(function (permission) {
    //If the user accepts permission, we can initiate a message
    if (permission === "granted") {
      Let notification = new notification ('sironan '{
        Body: 'sylsa Ronan'
      });
    }
  })
}

Push API Foundation

The push API implements the ability of web to accept messages pushed by the server. The specific implementation code of push API can be seen in my example, which realizes a simple push.

The client generates the subscription information and sends it to the server for saving. The server can use the subscription information to send push to the client when appropriate.

https://github.com/peoplesing…

Background sync API Foundation

A service worker thread, an API for periodically updating data.

Originally, I wanted to experiment with this API, but when I registered the timer task, I was prompted with an error of “domexception: permission denied.” this has not been solved for the time being.

ServiceWorkerContainer

The service worker thread does not have a global constructor navigator.serviceWorker Create, destroy, service worker thread

Creating a service worker thread

Like the shared worker thread, a new link is created when it does not exist. If the thread already exists, it is linked to the existing thread.

//Creating a service worker thread
navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js');

Register f returns a promise object. After the first call to register on the same page, subsequent calls to register do not return anything.

navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(() => {
  console.info ('registration successful ')
}).catch(() => {
  console.error ('registration failed ')
})

If the service worker thread is used to manage the cache, the service worker thread should register in advance in the page. Otherwise, the service worker thread should register in the load event.

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(() => {
      console.info ('registration successful ')
    }).catch(() => {
      console.error ('registration failed ')
    })
  });
}

Using the serviceworkercontainer object (container object)

Serviceworkercontainer object is the top encapsulation of service worker thread by browser. Serviceworkercontainer can be used in client through navigator.serviceWorker visit

  • navigator.serviceWorker.oncontrollerchange , triggered when registering a new service worker thread (to be exact, triggered when registering a new version of the service worker thread and taking over the page)
const btn = document.getElementById('btn')

btn.onclick = () => {
    navigator.serviceWorker.register('./sw2.js')
}

navigator.serviceWorker.oncontrollerchange = () => {
    console.log ('trigger controllerchange event ')
    // sw2
    console.log(navigator.serviceWorker.controller)
}

navigator.serviceWorker.register('./worker.js')
// sw1
console.log('hehe')
// sw2
self.addEventListener('install', async () => {
    //Force into activated state
    self.skipWaiting();
})

self.addEventListener('activate', async () => {
    //Mandatory takeover of client
    self.clients.claim();
})
  • Onerror, a service worker thread, is triggered when an error occurs
  • Onmessage, which monitors the message sent by the service worker thread and triggers when the message is received
  • Ready, return promise, and the content is the activated serviceworkerregistration object
  • Controller, returns the serviceworker object of the current page, and null if there is no activated service worker thread
  • Register (), create and update the serviceworkerregistration object
  • Getregistration (SCOPE), which returns promise. The content is associated with the serviceworkercontainer and matches the scope (path).
  • Getregistrations() returns promise, which contains all serviceworkerregistration objects associated with serviceworkercontainer
  • Startmessage () starts to accept messages sent by the service worker thread through PostMessage. If you do not use startmessage, Client.postMessage () the dispatched message will be scheduled after the domcontentloaded event. Startmessage can be scheduled as early as possible. (use se rviceWorkerContainer.onmessage The message will be sent automatically without startmessage)

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    let sw1;
    let sw2;
    navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(sw => {
      console.log(sw);
      sw1 = sw;
    }) 
    navigator.serviceWorker.ready.then((sw) => {
      console.log(sw);
      sw2 = sw;
    })
    setTimeout(() => {
      // true
      console.log(sw1 === sw2)
    }, 1000)
  });
}

Using the serviceworkerregistration object (Registration object)

Service Worker Registration, which represents the service worker thread that was successfully registered.It can be accessed through the promise returned by register. Call register on the same page. If the URL is the same, the same serviceworkerregistration object will be returned.

navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(sw => {
  //Serviceworkerregistration object
  console.log(sw);
})
  • On update found, state trigger in thread installation
  • Scope, which returns the path of the service worker thread
  • Navigationpreload, which returns the navigationpreloadmanager object associated with the registered object
  • Pushmanager, which returns the pushmanager object associated with the registered object (mainly used for registering messages to facilitate the server to push messages)
  • Install, returns the service worker thread with the status of installing
  • Waiting, returns the service worker thread whose status is waiting
  • Active, if any, returns the service worker thread with the status of activating or active
  • Shownotifications to display notifications. You can configure the title and body
  • Update, update serviceworker
  • Unregister, cancel serviceworker

Using serviceworker objects

How to get serviceworker object? There are two ways

  1. Active property of serviceworkerregistration object
  2. Controller property of serviceworkercontainer object

The serviceworker object inherits work, but does not contain the terminate method

  • onstatechange, ServiceWorker.state Trigger when changing
  • Scripturl, which returns the full URL of the registered worker thread. The relative path will be resolved to the full path
  • State, return the status of worker thread, installing, installed, activating, activated, redundant

Security restrictions for serviceworker

  1. Limited by homology
  2. It can only be used for HTTPS protocol, and HTTP protocol can be used under 127.0.0.1 or localhost.

If the protocol is not HTTPS, navigator.serviceWorker It’s undefined. window.isSecureContext You can determine whether the current context is secure.

ServiceWorkerGlobalScope

Inside the service worker thread, the global object is an instance of serviceworkerglobalscope. Serviceworkerglobalscope inherits workerglobalscope and therefore has its properties and methods. Thread internal access to the global context through self. The instance of serviceworkerglobalscope is extended as follows.

  • Caches, returns the cachestorage object
  • Clients, which returns the clients interface of the worker thread
  • Registration, which returns the registration object serviceworkerregistration of the service worker thread
  • Skipwaiting to force the worker thread into an active state
  • Fetch, in the service worker thread, is used to initiate a network request

Dedicated worker threads and shared worker threads only have one onmessage event as input, but service worker threads can accept multiple events

  • self.onintall , triggered when the service worker thread enters the installation (state before activating)
  • self.onactive , triggered when the service worker thread enters the activation state (state after activating)
  • self.onfetch , the service worker thread can intercept the fetch request of the page
  • self.onmessage , which is triggered when the service worker thread receives the message sent by PostMessage
  • self.onnotificationclick , users click servi ceWorkerRegistration.showNotification (), generated notification triggers
  • self.onnotificationclose , the user closes servi ceWorkerRegistration.showNotification (), generated notification triggers
  • self.onpush , which is triggered when the message pushed by the server is received

Limitation of service worker thread scope

//  worker.js Under the root line
navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js')
//  http://127.0.0.1 : all requests under 8080 will be blocked
fetch('http://127.0.0.1:8080/foo.js');
fetch('http://127.0.0.1:8080/foo/fooScript.js');
fetch('http://127.0.0.1:8080/baz/bazScript.js');

//  worker.js In the foo directory
navigator.serviceWorker.register('http://127.0.0.1:8080/foo/worker.js'})
//Requests in the foo directory will be blocked
fetch('/foo/fooScript.js')
//Requests from other paths will not be blocked
fetch('/foo.js')
fetch('/baz/bazScript.js')

If you want to exclude requests under a path, you can use the path with a slash at the end

//All requests in the foo path will not be intercepted
navigator.serviceWorker.register(
  'http://127.0.0.1:8080/worker.js',
  {
    scope: '/foo/'
  }
)

Service worker thread cache

  1. The service worker thread does not automatically cache any requests
  2. The service worker thread will not expire automatically
  3. The cache must be updated and deleted manually
  4. Cache versions must be managed manually
  5. The caching policy is LRU, when the cached data exceeds the browser limit

CacheStorage

Through self.caches Access the cachestorage object. Cachestorage is the mapping between string and cache object. Cachestorage can be accessed in pages or other worker threads.

//Access the cache. If there is no cache, it is created
self.caches.open(key)

Cachestorage also has map like APIs, such as has, delete, keys (), but they all return promise

match,matchAll

Return the first matched response respectively

(async () => {
    const request = new Request('https://www.foo.com')
    const response1 = new Response('fooResponse1')
    const response2 = new Response('fooResponse2')
    const v1 = await caches.open('v1')
    await v1.put(request, response1)
    const v2 = await caches.open('v2')
    await v2.put(request, response2)
    const matchCatch = await caches.match(request)
    const matchCatchText = await matchCatch.text()
    // true
    console.log(matchCatchText === 'fooResponse1')
})();  

Cache

The cachestorage object is a mapping between a string and a cache object. Cache object is the mapping between request object or URL string and response object.

  • Put, put (request, response) adds the cache and returns promise
  • Add (request), use add to send a fetch request and cache the response
  • Addall (request [] will call add for each item in the array.

Cache also has methods such as delete (), keys (), which return promise

(async () => {
    const request1 = new Request('https://www.foo.com');
    const response1 = new Response('fooResponse');
    const cache = await caches.open('v1')
    await cache.put(request1, response1)
    const keys = await cache.keys()
    // [Request]
    console.log(keys)
})()
  • Matchall, returns the matched response array
  • Match to return the matched response object
(async () => {
    const request1 = new Request('https://www.foo.com?a=1&b=2')
    const request2 = new Request('https://www.bar.com?a=1&b=2', {
        method: 'GET'
    })
    const response1 = new Response('fooResponse')
    const response2 = new Response('barResponse')
    const v3 = await caches.open('v3')
    await v3.put(request1, response1)
    await v3.put(request2, response2)
    const matchResponse = await v3.match(new Request('https://www.foo.com'), {
        Ignoremethod: true, // ignore matching get or post methods
        Ignoresearch: true, // ignore matching query string
    });
    const matchResponseText = await matchResponse.text()
    // fooResponse
    console.log(matchResponseText)
})();

The key and value of the catch object are copies created by the clone method of the request and response objects

(async () => {
    const request = new Request('https://www.foo.com');
    const response = new Response('fooResponse');
    const cache = await caches.open('v1')
    await cache.put(request, response)
    const keys = await cache.keys()
    // false
    console.log(keys[0] === request)
})();  

Maximum storage space

Get the storage space and the space currently used

navigator.storage.estimate()

Client

  • ID, the global unique identifier of the client
  • Type, the type of the client
  • URL, the URL of the client
  • PostMessage to send a message to a single client
  • Claim, which forces the worker thread to control all clients in the scope. When a serviceworker is initially registered, the page will not use it until it is next loaded. The claim () method immediately controls these pages.

On the problem of service worker thread controlling client

When you first register a service worker, the page will use it before the next load. There are two ways to control the page ahead of time

//Page
navigator.serviceWorker.register('./worker.js').then((registration) => {
  setTimeout(() => {
    fetch('/aa')
  }, 2000)
}).catch(() => {
  console.log ('registration failed ')
});
// sw
self.addEventListener('fetch', () => {
  //SW does not control the client, so it cannot intercept the fetch request and throw an error
  Throw new error ('ha ha ')
})

The first solution is to use claim to enforce control, but it may cause version resource inconsistency

self.addEventListener('activate', async () => {
    self.clients.claim();
})

self.addEventListener('fetch', () => {
    //You can throw an error
    Throw new error ('ha ha ')
})

The second solution is to refresh the page

navigator.serviceWorker.register('./worker.js').then((registration) => {
    setTimeout(() => {
        fetch('/aa')
    }, 3000)
    registration.addEventListener('updatefound', () => {
        const sw = registration.installing;
        sw.onstatechange = () => {
            console.log('sw.state', sw.state)
            if (sw.state === 'activated') {
                console.log ('refresh page ')
                //You can throw an error after refreshing the page
                window.location.reload();
            }
        }     
    })
}).catch(() => {
    console.log ('registration failed ')
});

Consistency of service worker threads

The most important thing for service worker threads is to maintain consistency (there will be no service worker threads with V1 version for page A and V2 version for page b).

  1. Code consistency, service worker threads in all tabs with a version of the code
  2. Data consistency,

    • Service worker thread fails early: syntax error and resource loading failure will cause service worker thread loading to be faster than
    • Radical update of service worker thread: any difference between the loaded service worker thread or the resources that the service worker thread relies on will result in the installation of a new version of service worker thread (the newly installed service worker thread will enter the installed state)
    • The service worker thread is inactive. After using register to install the service worker thread, the service worker thread will be installed but will not be activated (unless all tabs controlled by the previous version are closed or called) self.skipWaiting Methods)
    • If at least one client is associated with the active service worker thread, the browser will use it in all pages of the source. For the new version of the service, the worker thread will wait all the time.

life cycle

  1. Parsed
  2. Installing
  3. Installed
  4. Activating
  5. Activated
  6. Invalid

Parsed

Call navigator.serviceWorker.register () will enter the resolved state, but there are no events and no corresponding ServiceWorker.state The value of.

Installing

In the client can pass the check registration.installing Whether it is set as a serviceworker instance to determine whether it is in the state of installation. When the service worker thread reaches the state in installation, the onupdatefound event is triggered.

navigator.serviceWorker.register('./sw1.js').then((registration) => {
    registration.onupdatefound = () => {
        console.log ('I have reached the state of installing ')
    }
    console.log(registration.installing)
});

Inside the service worker thread, you can determine the status of the installation by listening to the install event.

In the install event, it can be used to fill the cache. The waituntil method can be used. The waituntil method accepts a promise. Only when promise returns resolve, the state of the service worker thread will transition to the next state.

self.addEventListener('install', (event) => {
    event.waitUntil(async () => {
        const v1 = await caches.open('v1')
        //After the completion of the cache resource, it will transition to the next state
        v1.addAll([
            'xxxx.js',
            'xxx.css'
        ])
    })
})

Installed

In the client can pass the check registration.waiting Whether it is set as a serviceworker instance to determine whether it is installed. If there is no previous version of serviceworker in the browser, the newly installed serviceworker will directly skip this state and enter the active state. Otherwise, it will wait.

navigator.serviceWorker.register('./worker.js').then((registration) => {
    console.log(registration.waiting)
});

If you have an installed serviceworker, you can use the self.skipWaiting To force the worker thread into an active state

Activating

If there is no previous version of serviceworker in the browser, the new service worker thread will enter this state directly. If there are other server threads, you can activate the new server thread by the following method

  1. self.skipWaiting (), forced into the active state
  2. The number of clients of the original service worker thread is changed to 0 (all tabs are closed) and the new worker thread enters the active state in the next navigation event.
const btn = document.getElementById('btn');

navigator.serviceWorker.register('./sw1.js').then((registration) => {
    //The first time a service worker process without activity (previous version) is loaded, waiting will skip directly, so it is null
    console.log('waiting:', registration.waiting);
    //The service worker thread of SW1 is currently activated
    console.log('active:', registration.active);
});

btn.onclick = () => {
    navigator.serviceWorker.register('./sw2.js').then((registration) => {
        //Load the new version of the service worker thread and trigger the update load
        //Because there are already active service worker threads before, the thread in waiting state is SW2
        console.log('waiting:', registration.waiting);
        //The active state is the thread of SW1
        console.log('active:', registration.active);
    })
}

Advanced JavaScript programming for reading notes 4th Edition (Chapter 27)

In the client, it can be roughly judged by the registration.active Whether it is an instance of serviceworker. (active is an instance of serviceworker, which may be active or in active state)

In the service worker thread, you can judge by adding an activate event handler, which is often used to delete the previous cache


const CATCH_KEY = 'v1'

self.addEventListener('activate', async (event) => {
  const keys = await caches.keys();
  keys.forEach((key) => {
    if (key !== CATCH_KEY) {
      caches.delete(key)
    }
  });
})

Note: the activation event does not mean that the page is under control. have access to clients.claim () controls uncontrolled clients.

Activated state activated

In the client, it can be roughly judged by the registration.active Whether it is an instance of serviceworker. (active is an instance of serviceworker, which can be active or in active state)

Or you can view the registration.controller Property, and the controller property returns the instance of the activated serviceworker. This is triggered when a new service worker thread controls the client navigator.serviceWorker.oncontrollerchange event

perhaps navigator.serviceWorker.ready When the promise returned is resolve, the worker thread is also activated.

const btn = document.getElementById('btn');

navigator.serviceWorker.register('./sw1.js').then((registration) => {
    //Activated thread SW1
    console.log('activated', navigator.serviceWorker.controller)
});

btn.onclick = () => {
    navigator.serviceWorker.register('./sw2.js').then((registration) => {
        //Waiting thread SW2
        console.log('waiting', registration.waiting)
        //Activated thread SW1
        console.log('activated', navigator.serviceWorker.controller)
    })
}

Invalid state

The service worker will be destroyed and recycled by the browser

Update service worker thread

The following actions trigger the update check:

  1. use navigator.serviceWorker.register (), load different URLs, check
  2. A push, fetch event occurred. And there is no update check for at least 24 hours.
  3. The browser navigates to a page in the scope of the service worker thread.

If the update check finds the difference, the browser will use the new script to initialize the new worker thread, and the new worker thread will reach the state of installed. Then it will wait. Unless used self.skipWaiting (), forced into the active state. Or the number of clients of the original service worker thread becomes 0 (all tabs are closed) and the new worker thread enters the active state in the next navigation event.

Refreshing the page does not activate the update service worker thread and replace the existing service worker thread. For example, there is an open page where a service worker thread is controlling it and an update service worker thread is waiting in the installed state. The client will overlap during page refresh, that is, the old page has not been unloaded and the new page has been loaded. Therefore, the existing service worker thread will never give up control. After all, there is at least one client under its control. Therefore, the only way to replace the existing service worker thread is to close all controlled pages.

updateViaCache

Using updateviacache, you can control the caching of service worker threads

  • None, the scripts of the service worker thread and the introduction of importscripts will not be cached
  • All, all files will be cached by HTTP
  • The scripts of the service worker thread will not be cached, and the files of importscripts will be cached by HTTP

navigator.serviceWorker.register('/serviceWorker.js', {
  updateViaCache: 'none'
});

Service worker thread and page communication

//Page
navigator.serviceWorker.onmessage = ({ data }) => {
    console.log ('message sent by server thread: ', data');
}

navigator.serviceWorker.register('./worker.js').then((registration) => {
    console.log ('registration successful ')
}).catch(() => {
    console.log ('registration failed ')
});
// sw
self.addEventListener('install', async () => {
    self.skipWaiting();
});

self.addEventListener('activate', async () => {
    self.clients.claim();
    const allClients = await clients.matchAll({
        includeUncontrolled: true
    });
    let homeClient = null;
    for (const client of allClients) {
        const url = new URL(client.url);
        if (url.pathname === '/') {
            homeClient = client;
            break;
        }
    }
    homeClient.postMessage('Hello')
});

Intercept fetch request

self.onfetch = (fetchEvent) => {
  fetchEvent.respondWith(fetch(fetchEvent.request));
};

fetchEvent.respondWith Accept promise and return the repose object,

Return from cache


self.onfetch = (fetchEvent) => {
  fetchEvent.respondWith(caches.match(fetchEvent.request));
};

Return from network, cache as backup

self.onfetch = (fetchEvent) => {
  fetchEvent.respondWith(fetch(fetchEvent.request).catch(() => {
    return caches.match(fetchEvent.request)
  }));
};

Return from cache, network as backup

self.onfetch = (fetchEvent) => {
  fetchEvent.respondWith(
    caches.match(fetchEvent.request).then((response) => {
      return response || fetch(fetchEvent.request).then(async (res) => {
        //After the network returns successfully, the resources returned by the network are cached locally
        const catch = await catchs.open('CatchName')
        await catch.put(fetchEvent.request, res)
        return res;
      })
    })
  );
};

General backup

When the server thread loads, the resource should be cached. When the cache and network fail, the general backup should be returned.

reference resources