第27章 工作者进程
# 工作者线程简介
JavaScript 环境实际上是运行在托管操作系统中的虚拟环境。在浏览器中每打开一个页面,就会分配一个它自己的环境。这样,每个页面都有自己的内存、事件循环、DOM,等等。每个页面就相当于一个沙盒,不会干扰其他页面。对于浏览器来说,同时管理多个环境是非常简单的,因为所有这些环境都是并行执行的。
使用工作者线程,浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境。这个子环境不能与依赖单线程交互的 API(如 DOM)互操作,但可以与父环境并行执行代码。
# 工作者线程与线程
工作者线程和线程有很多共同之处:
- 工作者线程是以实际线程实现的。
- 工作者线程并行执行。
- 工作者线程可以共享某些内存。
也有区别:
工作者线程不共享全部内存。
工作者线程不一定在同一个进程里。
创建工作者线程的开销更大。工作者线程有自己独立的事件循环、全局对象、事件处理程序和
其他 JavaScript 环境必需的特性。
工作者线程应该是长期运行的,启动成本比较高,每个实例占用的内存也比较大。
# 工作者线程的类型
有了三种主要的工作者线程:
专用工作者线程:简称Web Worker,可以让脚本单独创建一个 JavaScript 线程,以执行委托的任务。只能被创建它的页面使用。
共享工作者线程:共享工作者线程与专用工作者线程非常相似。主要区别是共享工作者线程可以被多个不同的上下文使用,包括不同的页面。
服务工作者线程:它的主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色。
# WorkerGlobalScope
在工作者线程内部,没有 window的概念。这里的全局对象是 WorkerGlobalScope 的实例,通过 self 关键字暴露出来。
WorkerGlobalScope 属性和方法:
navigator:返回与工作者线程关联的 WorkerNavigator。
self:返回 WorkerGlobalScope 对象。
location:返回与工作者线程关联的 WorkerLocation。
performance:返回(只包含特定属性和方法的)Performance 对象。
console:返回与工作者线程关联的 Console 对象;对 API 没有限制。
caches:返回与工作者线程关联的 CacheStorage 对象;对 API 没有限制。
indexedDB:返回 IDBFactory 对象。
isSecureContext:返回布尔值,表示工作者线程上下文是否安全。
atob()
btoa()
clearInterval()
clearTimeout()
createImageBitmap()
fetch()
setInterval()
setTimeout()
# 专用工作者线程(web worker)
Web worker可以称为后台脚本,它可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现其他不适合在页面执行线程里做的任务。
# 基本概念
- 创建空的专用工作者线程:
// emptyWorker.js
// 空的 JS 工作者线程文件
// main.js
console.log(location.href); // "https://example.com/"
const worker = new Worker(location.href + 'emptyWorker.js');
console.log(worker); // Worker {}
2
3
4
5
6
7
emptyWorker.js 文件可以使用绝对路径和相对路径加载。
emptyWorker.js 是在后台加载的,工作者线程的初始化完全独立于 main.js。
main.js 中必须以 Worker 对象为代理实现与工作者线程通信。
- 工作者线程的脚本文件只能从与父页面相同的源加载。从其他源加载工作者线程的脚本文件会导致错误。
// 尝试基于 https://untrusted.com/worker.js 创建工作者线程
const remoteOriginWorker = new Worker('https://untrusted.com/worker.js'); // 报错
2
- 使用 Worker 对象
Worker()构造函数返回的 Worker 对象是与刚创建的专用工作者线程通信的连接点。它可用于在工作者线程和父上下文间传输信息,以及捕获专用工作者线程发出的事件。
Worker 对象支持下列事件处理程序属性。
- onerror:在工作者线程中发生 ErrorEvent 类型的错误事件时会调用。
- onmessage:在工作者线程中发生 MessageEvent 类型的消息事件时会调用。
- onmessageerror:在工作者线程中发生 MessageEvent 类型的错误事件时会调用
Worker 对象还支持下列方法。
- postMessage():用于通过异步消息事件向工作者线程发送信息。
- terminate():用于立即终止工作者线程。
- DedicatedWorkerGlobalScope
在专用工作者线程内部,全局作用域是 DedicatedWorkerGlobalScope 的实例。它继承自WorkerGlobalScope。工作者线程可以通过 self 关键字访问该全局作用域。
// globalScopeWorker.js
console.log('inside worker:', self);
// main.js
const worker = new Worker('./globalScopeWorker.js');
console.log('created worker:', worker);
// created worker: Worker {}
// inside worker: DedicatedWorkerGlobalScope {}
2
3
4
5
6
7
DedicatedWorkerGlobalScope 在 WorkerGlobalScope 基础上增加了以下属性和方法。
name:可以提供给 Worker 构造函数的一个可选的字符串标识符。
postMessage():与 worker.postMessage()对应的方法,用于从工作者线程内部向父上下
文发送消息。
- close():与 worker.terminate()对应的方法,用于立即终止工作者线程。没有为工作者线
程提供清理的机会,脚本会突然停止。
- importScripts():用于向工作者线程中导入任意数量的脚本。
# 生命周期
- 生命周期状态
专用工作者线程可以非正式区分为处于下列三个状态:初始化(initializing)、活动(active) 和终止(terminated)。
初始化时,虽然工作者线程脚本尚未执行,但可以先把要发送给工作者线程的消息加入队列。
// initializingWorker.js
self.addEventListener('message', ({data}) => console.log(data));
// main.js
const worker = new Worker('./initializingWorker.js');
// Worker 可能仍处于初始化状态
// 但 postMessage()数据可以正常处理
worker.postMessage('foo');
worker.postMessage('bar');
worker.postMessage('baz');
// foo
// bar
// baz
2
3
4
5
6
7
8
9
10
11
12
在整个生命周期中,一个专用工作者线程只会关联一个网页(或称为一个文档)。除非明确终止,否则只要关联文档存在,专用工作者线程就会存在。如果浏览器离开网页(通过导航或关闭标签页或关闭窗口),它会将与其关联的工作者线程标记为终止,它们的执行也会立即停止。
- 行内创建web worker
专用工作者线程也可以通过 Blob 对象 URL 在行内脚本创建。这样可以更快速地初始化工作者线程,因为没有网络延迟。
// 创建要执行的 JavaScript 代码字符串
const workerScript = `
self.onmessage = ({data}) => console.log(data);
`;
// 基于脚本字符串生成 Blob 对象
const workerScriptBlob = new Blob([workerScript]);
// 基于 Blob 实例创建对象 URL
const workerScriptBlobUrl = URL.createObjectURL(workerScriptBlob);
// 基于对象 URL 创建专用工作者线程
const worker = new Worker(workerScriptBlobUrl);
worker.postMessage('blob worker script');
// blob worker script
2
3
4
5
6
7
8
9
10
11
12
- 在工作者线程中动态执行脚本
可以使用 importScripts()方法通过编程方式加载和执行任意脚本。该方法可用于全局 Worker 对象。这个方法会加载脚本并按照加载顺序同步执行。
main.js
const worker = new Worker('./worker.js');
// importing scripts
// scriptA executes
// scriptB executes
// scripts imported
scriptA.js
console.log('scriptA executes');
scriptB.js
console.log('scriptB executes');
worker.js
console.log('importing scripts');
importScripts('./scriptA.js');
importScripts('./scriptB.js');
console.log('scripts imported');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 所有导入的脚本会共享作用域。(见globalToken变量。)
main.js
const worker = new Worker('./worker.js', {name: 'foo'});
// importing scripts in foo with bar
// scriptA executes in foo with bar
// scriptB executes in foo with bar
// scripts imported
scriptA.js
console.log(`scriptA executes in ${self.name} with ${globalToken}`);
scriptB.js
console.log(`scriptB executes in ${self.name} with ${globalToken}`);
worker.js
const globalToken = 'bar';
console.log(`importing scripts in ${self.name} with ${globalToken}`);
importScripts('./scriptA.js', './scriptB.js');
console.log('scripts imported');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 委托任务到子工作者线程
有时候可能需要在工作者线程中再创建子工作者线程。
main.js
const worker = new Worker('./js/worker.js');
// worker
// subworker
js/worker.js
console.log('worker');
const worker = new Worker('./subworker.js');
js/subworker.js
console.log('subworker');
2
3
4
5
6
7
8
9
- 处理工作者线程错误
main.js
try {
const worker = new Worker('./worker.js');
console.log('no error');
worker.onerror = console.log;
} catch(e) {
console.log('caught error');
}
// no error
// ErrorEvent {message: "Uncaught Error: foo"}
worker.js
throw Error('foo');
2
3
4
5
6
7
8
9
10
11
12
13
- 与专用工作者线程通信
使用 postMessage()
factorialWorker.js function factorial(n) { let result = 1; while(n) { result *= n--; } return result; } self.onmessage = ({data}) => { self.postMessage(`${data}! = ${factorial(data)}`); }; main.js const factorialWorker = new Worker('./factorialWorker.js'); factorialWorker.onmessage = ({data}) => console.log(data); factorialWorker.postMessage(5); factorialWorker.postMessage(7); factorialWorker.postMessage(10); // 5! = 120 // 7! = 5040 // 10! = 3628800
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20使用 MessageChannel
MessageChannel 实例有两个端口,分别代表两个通信端点。要让父页面和工作线程通过MessageChannel 通信,需要把一个端口传到工作者线程中。
worker.js // 在监听器中存储全局 messagePort let messagePort = null; function factorial(n) { let result = 1; while(n) { result *= n--; } return result; } // 在全局对象上添加消息处理程序 self.onmessage = ({ports}) => { // 只设置一次端口 if (!messagePort) { // 初始化消息发送端口, // 给变量赋值并重置监听器 messagePort = ports[0]; self.onmessage = null; // 在全局对象上设置消息处理程序 messagePort.onmessage = ({data}) => { // 收到x消息后发送数据 messagePort.postMessage(`${data}! = ${factorial(data)}`); }; } }; main.js const channel = new MessageChannel(); const factorialWorker = new Worker('./worker.js'); // 把`MessagePort`对象发送到工作者线程 // 工作者线程负责处理初始化信道 factorialWorker.postMessage(null, [channel.port1]); // 通过信道实际发送数据 channel.port2.onmessage = ({data}) => console.log(data); // 工作者线程通过信道响应 channel.port2.postMessage(5); // 5! = 120
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35MessageChannel 真正有用的地方是让两个工作者线程之间直接通信,通过把端口传给另一个工作者线程实现。
main.js const channel = new MessageChannel(); const workerA = new Worker('./worker.js'); const workerB = new Worker('./worker.js'); workerA.postMessage('workerA', [channel.port1]); workerB.postMessage('workerB', [channel.port2]); workerA.onmessage = ({data}) => console.log(data); workerB.onmessage = ({data}) => console.log(data); workerA.postMessage(['page']); // ['page', 'workerA', 'workerB'] workerB.postMessage(['page']) // ['page', 'workerB', 'workerA'] worker.js let messagePort = null; let contextIdentifier = null; function addContextAndSend(data, destination) { // 添加标识符以标识当前工作者线程 data.push(contextIdentifier); // 把数据发送到下一个目标 destination.postMessage(data); } self.onmessage = ({data, ports}) => { // 如果消息里存在端口(ports) // 则初始化工作者线程 if (ports.length) { // 记录标识符 contextIdentifier = data; // 获取 MessagePort messagePort = ports[0]; // 添加处理程序把接收的数据 // 发回到父页面 messagePort.onmessage = ({data}) => { addContextAndSend(data, self); } } else { addContextAndSend(data, messagePort); } };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39使用 BroadcastChannel
这种通道类型的设置比较简单。
main.js
const channel = new BroadcastChannel('worker_channel');
const worker = new Worker('./worker.js');
channel.onmessage = ({data}) => {
console.log(`heard ${data} on page`);
}
setTimeout(() => channel.postMessage('foo'), 1000);
// heard foo in worker
// heard bar on page
worker.js
const channel = new BroadcastChannel('worker_channel');
channel.onmessage = ({data}) => {
console.log(`heard ${data} in worker`);
channel.postMessage('bar');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 线程池
某些情况下可以考虑始终保持固定数量的线程活动,需要时就把任务分派给它们。工作者线程在执行计算时,会被标记为忙碌状态。直到它通知线程池自己空闲了,才准备好接收新任务。这些活动线程就称为“线程池”或“工作者线程池”。
# 共享工作者线程(略)
# 服务工作者线程(service worker)
服务工作者线程(service worker)是一种类似浏览器中代理服务器的线程,可以拦截外出请求和缓存响应。
服务工作者线程在两个主要任务上最有用:充当网络请求的缓存层和启用推送通知。
# 服务工作者线程基础
- ServiceWorkerContainer
服务工作者线程没有全局构造函数,而是通过 ServiceWorkerContainer 来管理的。
console.log(navigator.serviceWorker);
// ServiceWorkerContainer { ... }
2
- 创建服务工作者线程
service worker在还不存在时创建新实例,在存在时连接到已有实例。通过register()方法创建。
emptyServiceWorker.js
// 空服务脚本
main.js
// 注册成功,成功回调(解决)
navigator.serviceWorker.register('./emptyServiceWorker.js')
.then(console.log, console.error);
// ServiceWorkerRegistration { ... }
// 使用不存在的文件注册,失败回调(拒绝)
navigator.serviceWorker.register('./doesNotExist.js')
.then(console.log, console.error);
// TypeError: Failed to register a ServiceWorker:
// A bad HTTP response code (404) was received when fetching the script.
2
3
4
5
6
7
8
9
10
11
12
- 使用 ServiceWorkerContainer 对象
ServiceWorkerContainer 支持以下事件处理程序。
oncontrollerchange:在 ServiceWorkerContainer 触发 controllerchange 事件时会调用指定的事件处理程序。
此事件在获得新激活的 ServiceWorkerRegistration 时触发。
onerror:在关联的服务工作者线程触发 ErrorEvent 错误事件时会调用指定的事件处理程序。
此事件在关联的服务工作者线程内部抛出错误时触发。
onmessage:在服务工作者线程触发 MessageEvent 事件时会调用指定的事件处理程序。
此事件在服务脚本向父上下文发送消息时触发。
ServiceWorkerContainer 支持下列属性。
ready:返回期约,解决为激活的 ServiceWorkerRegistration 对象。
controller:返回与当前页面关联的激活的 ServiceWorker 对象,如果没有激活的服务工作
者线程则返回 null。
ServiceWorkerContainer 支持下列方法。
register():使用接收的 url 和 options 对象创建或更新 ServiceWorkerRegistration。
getRegistration():返回期约,解决为与提供的作用域匹配的 ServiceWorkerRegistration
对象;如果没有匹配的服务工作者线程则返回 undefined。
- getRegistrations():返回期约,解决为与 ServiceWorkerContainer 关联的 Service
WorkerRegistration 对象的数组;如果没有关联的服务工作者线程则返回空数组。
- startMessage():开始传送通过 Client.postMessage()派发的消息。
- 使用 ServiceWorkerRegistration 对象
ServiceWorkerRegistration 对象表示注册成功的服务工作者线程。该对象可以在 register()返回的解决期约的处理程序中访问到。
ServiceWorkerRegistration 支持以下事件处理程序。
onupdatefound:在服务工作者线程触发 updatefound 事件时会调用指定的事件处理程序。
此事件会在服务工作者线程开始安装新版本时触发,
ServiceWorkerRegistration 支持以下属性和方法。
- scope:返回服务工作者线程作用域的完整 URL 路径。该值源自接收服务脚本的路径和在
register()中提供的作用域。
navigationPreload:返回与注册对象关联的 NavigationPreloadManager 实例。
pushManager:返回与注册对象关联的 pushManager 实例。
installing:如果有则返回状态为 installing(安装)的服务工作者线程,否则为 null。
waiting:如果有则返回状态为 waiting(等待)的服务工作者线程,否则为 null。
active:如果有则返回状态 activating 或 active(活动)的服务工作者线程,否则为 null。
getNotifications():返回期约,解决为 Notification 对象的数组。
showNotifications():显示通知,可以配置 title 和 options 参数。
update():直接从服务器重新请求服务脚本,如果新脚本不同,则重新初始化。
unregister():取消服务工作者线程的注册。该方法会在服务工作者线程执行完再取消注册。
- 使用 ServiceWorker 对象
ServiceWorker 对象可以通过两种方式获得:通过 ServiceWorkerContainer 对象的 controller属性和通过 ServiceWorkerRegistration 的 active 属性。
ServiceWorker 支持以下事件处理程序和属性。
onstatechange:ServiceWorker 发生 statechange 事件时会调用指定的事件处理程序。
此事件会在 ServiceWorker.state 变化时发生。
scriptURL:解析后注册服务工作者线程的 URL。
state:表示服务工作者线程状态的字符串,可能的值有installing、installed、activating、activated、redundant。
- 服务工作者线程的安全限制
服务工作者线程几乎可以任意修改和重定向网络请求,以及加载静态资源,服务工作者线程API 只能在安全上下文(HTTPS)下使用。
# 服务工作者线程缓存
服务工作者线程的一个主要能力是可以通过编程方式实现真正的网络请求缓存机制。与 HTTP 缓存
或 CPU 缓存不同,服务工作者线程缓存非常简单。
服务工作者线程缓存不自动缓存任何请求。所有缓存都必须明确指定。
服务工作者线程缓存没有到期失效的概念。除非明确删除,否则缓存内容一直有效。
服务工作者线程缓存必须手动更新和删除。
缓存版本必须手动管理。每次服务工作者线程更新,新服务工作者线程负责提供新的缓存键以
保存新缓存。
- 唯一的浏览器强制逐出策略基于服务工作者线程缓存占用的空间。服务工作者线程负责管理自
己缓存占用的空间。缓存超过浏览器限制时,浏览器会基于最近最少使用(LRU,Least Recently
Used)原则为新缓存腾出空间。
本质上,服务工作者线程缓存机制是一个双层字典,其中顶级字典的条目映射到二级嵌套字典。顶级字典是 CacheStorage 对象,可以通过服务工作者线程全局作用域的 caches 属性访问。顶级字典中的每个值都是一个 Cache 对象,该对象也是个字典,是 Request 对象到 Response 对象的映射。
与 LocalStorage 一样,Cache 对象在 CacheStorage 字典中无限期存在。
- CacheStorage 对象
CacheStorage 对象是映射到 Cache 对象的字符串键/值存储。API类似于异步 Map,可以通过全局对象的 caches 属性访问。
caches.open('v1').then(console.log);
// Cache {}
2
与 Map 类似,CacheStorage 也有 has()、delete()和 keys()方法。这些方法与 Map 上对应方法类似,但都基于期约。
// 打开新缓存 v1
// 检查缓存 v1 是否存在
// 删除缓存 v1
// 再次检查缓存 v1 是否存在
caches.open('v1')
.then(() => caches.has('v1'))
.then(console.log) // true
.then(() => caches.delete('v1'))
.then(() => caches.has('v1'))
.then(console.log); // false
// 打开缓存 v1、v3 和 v2
// 检查当前缓存的键
// 注意:缓存键按创建顺序输出
caches.open('v1')
.then(() => caches.open('v3'))
.then(() => caches.open('v2'))
.then(() => caches.keys())
.then(console.log); // ["v1", "v3", "v2"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- Cache 对象
Cache 对象跟 CacheStorage 一样,类似于异步的 Map。Cache 键可以是 URL 字符串,也可以是 Request 对象。这些键会映射到 Response 对象。
默认情况下,Cache 只考虑缓存 HTTP 的 GET 请求,不允许使用 POST、PUT 和 DELETE 等请求方法。
为填充 Cache,可能使用以下三个方法。
put(request, response):在键(Request 对象或 URL 字符串)和值(Response 对象)同时存在时用于添加缓存项。该方法返回期约,在添加成功后会解决。
add(request):在只有 Request 对象或 URL 时使用此方法发送 fetch()请求,并缓存响应。该方法返回期约,期约在添加成功后会解决。
addAll(requests):在希望填充全部缓存时使用,比如在服务工作者线程初始化时也初始化缓存。该方法接收 URL 或 Request 对象的数组。addAll()会对请求数组中的每一项分别调用add()。该方法返回期约,期约在所有缓存内容添加成功后会解决。
与 Map 类似,Cache 也有 delete()和 keys()方法。这些方法与 Map 上对应方法类似,但都基于期约。
const request1 = new Request('https://www.foo.com');
const response1 = new Response('fooResponse');
caches.open('v1')
.then((cache) => {
cache.put(request1, response1)
.then(() => cache.keys())
.then(console.log) // [Request]
.then(() => cache.delete(request1))
.then(() => cache.keys())
.then(console.log); // []
});
2
3
4
5
6
7
8
9
10
11
要检索 Cache,可以使用下面的两个方法。
matchAll(request, options):返回期约,期约解决为匹配缓存中 Response 对象的数组。
此方法对结构类似的缓存执行批量操作,比如删除所有缓存在/images 目录下的值。
match(request, options):返回期约,期约解决为匹配缓存中的 Response 对象;如果没命中缓存则返回 undefined。
本质上相当于 matchAll(request, options)[0]。
缓存是否命中取决于 URL 字符串和/或 Request 对象 URL 是否匹配。
const request1 = 'https://www.foo.com';
const request2 = new Request('https://www.bar.com');
const response1 = new Response('fooResponse');
const response2 = new Response('barResponse');
caches.open('v1').then((cache) => {
cache.put(request1, response1)
.then(() => cache.put(request2, response2))
.then(() => cache.match(new Request('https://www.foo.com')))
.then((response) => response.text())
.then(console.log) // fooResponse
.then(() => cache.match('https://www.bar.com'))
.then((response) => response.text())
.then(console.log); // barResponse
});
2
3
4
5
6
7
8
9
10
11
12
13
14
- 最大存储空间
此方法只在安全上下文中可用,且并不是确切的数值。
navigator.storage.estimate()
.then(console.log);
// 不同浏览器的输出可能不同:
// { quota: 2147483648, usage: 590845 }
2
3
4
# 服务工作者线程客户端
Client 对象支持以下属性和方法。
id:返回客户端的全局唯一标识符。id可用于通过 Client.get()获取客户端的引用。
type:返回表示客户端类型的字符串。type 可能的值是 window、worker 或 sharedworker。
url:返回客户端的 URL。
postMessage():用于向单个客户端发送消息。
# 服务工作者线程的生命周期
Service Worker 规范定义了 6 种服务工作者线程可能存在的状态:已解析(parsed)、安装中(installing)、已安装(installed)、激活中(activating)、已激活(activated)和已失效(redundant)
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
registration.installing.onstatechange = ({ target: { state } }) => {
console.log('state changed to', state);
};
});
2
3
4
5
6
- 已解析状态
调用 navigator.serviceWorker.register()会启动创建服务工作者线程实例的过程。刚创建的服务工作者线程实例会进入已解析状态。该状态没有事件,也没有与之相关的 ServiceWorker.state 值。
- 安装中状态
安装中状态是执行所有服务工作者线程设置任务的状态。这些任务包括在服务工作者线程控制页面前必须完成的操作。
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
if (registration.installing) {
console.log('Service worker is in the installing state');
}
// 或者
registration.onupdatefound = () =>
console.log('Service worker is in the installing state');
};
});
2
3
4
5
6
7
8
9
10
11
- 已安装状态
已安装状态也称为等待中(waiting)状态,意思是服务工作者线程此时没有别的事件要做,只是准备在得到许可的时候去控制客户端。
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
if (registration.waiting) {
console.log('Service worker is in the installing/waiting state');
}
});
2
3
4
5
6
- 激活中状态
激活中状态表示服务工作者线程已经被浏览器选中即将变成可以控制页面的服务工作者线程。
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
if (registration.active) {
console.log('Service worker is in the activating/activated state');
}
});
2
3
4
5
6
- 已激活状态
已激活状态表示服务工作者线程正在控制一个或多个客户端。在这个状态,服务工作者线程会捕获其作用域中的 fetch()事件、通知和推送事件。
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
if (registration.active) {
console.log('Service worker is in the activating/activated state');
}
});
2
3
4
5
6
- 已失效状态
已失效状态表示服务工作者线程已被宣布死亡。不会再有事件发送给它,浏览器随时可能销毁它并回收它的资源。
# 控制反转与服务工作者线程持久化
虽然专用工作者线程和共享工作者线程是有状态的,但服务工作者线程是无状态的。更具体地说,服务工作者线程遵循控制反转(IoC,Inversion of Control)模式并且是事件驱动的。
意味着服务工作者线程不应该依赖工作者线程的全局状态。服务工作者线程中的绝大多数代码应该在事件处理程序中定义。
# 通过 updateViaCache 管理服务文件缓存
正常情况下,浏览器加载的所有 JavaScript 资源会按照它们的 Cache-Control 头部纳入 HTTP 缓存管理。因为服务脚本没有优先权,所以浏览器不会在缓存文件失效前接收更新的服务脚本。
为了尽可能传播更新后的服务脚本,常见的解决方案是在响应服务脚本时设置 Cache-Control: max-age=0 头部。这样浏览器就能始终取得最新的脚本文件。
为了让客户端能控制自己的更新行为,可以通过 updateViaCache 属性设置客户端对待服务脚本的方式。
- imports:默认值。顶级服务脚本永远不会被缓存,但通过 importScripts()在服务工作者线
程内部导入的文件会按照 Cache-Control 头部设置纳入 HTTP 缓存管理。
- all:服务脚本没有任何特殊待遇。所有文件都会按照 Cache-Control 头部设置纳入 HTTP 缓
存管理。
- none:顶级服务脚本和通过 importScripts()在服务工作者线程内部导入的文件永远都不会
被缓存。
可以像下面这样使用 updateViaCache 属性:
navigator.serviceWorker.register('/serviceWorker.js', {
updateViaCache: 'none'
});
2
3
# 强制性服务工作者线程操作
某些情况下,有必要尽可能快地让服务工作者线程进入已激活状态,即使可能会造成资源版本控制不一致。
const CACHE_KEY = 'v1';
self.oninstall = (installEvent) => {
// 填充缓存,然后强制服务工作者线程进入已激活状态
// 这样会触发 activate 事件
installEvent.waitUntil(
caches.open(CACHE_KEY)
.then((cache) => cache.addAll([
'foo.css',
'bar.js',
]))
.then(() => self.skipWaiting())
);
};
// 强制服务工作者线程接管客户端
// 这会在每个客户端触发 controllerchange 事件
self.onactivate = (activateEvent) => clients.claim();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ServiceWorkerRegistration对象提供了一个 update()方法,可以用来告诉浏览器去重新获取服务脚本。
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
// 每 17 分钟左右检查一个更新版本
setInterval(() => registration.update(), 1E6);
});
2
3
4
5
# 服务工作者线程消息
服务工作者线程也能与客户端通过 postMessage()交换消息。
ServiceWorker.js
self.onmessage = ({data, source}) => {
console.log('service worker heard:', data);
source.postMessage('bar');
};
main.js
navigator.serviceWorker.onmessage = ({data}) => {
console.log('client heard:', data);
};
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
if (registration.active) {
registration.active.postMessage('foo');
}
});
// service worker heard: foo
// client heard: bar
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 拦截 fetch 事件
服务工作者线程可以拦截网络请求,这种拦截能力不限于 fetch()方法发送的请求,也能拦截对 JavaScript、CSS、图片和HTML(包括对主 HTML 文档本身)等资源发送的请求。
- 从网络返回
简单的转发fetch事件。
self.onfetch = (fetchEvent) => {
fetchEvent.respondWith(fetch(fetchEvent.request));
};
2
3
- 从缓存返回
self.onfetch = (fetchEvent) => {
fetchEvent.respondWith(caches.match(fetchEvent.request));
};
2
3
- 从网络返回,缓存作后备
这个策略把从网络获取最新的数据作为首选,但如果缓存中有值也会返回缓存的值。如果应用程序需要尽可能展示最新数据,但在离线的情况下仍要展示一些信息,就可以采用该策略:
self.onfetch = (fetchEvent) => {
fetchEvent.respondWith(
fetch(fetchEvent.request)
.catch(() => caches.match(fetchEvent.request))
);
};
2
3
4
5
6
- 从缓存返回,网络作后备
这个策略优先考虑响应速度,但仍会在没有缓存的情况下发送网络请求。这是大多数渐进式 Web应用程序(PWA,Progressive Web Application)采取的首选策略:
self.onfetch = (fetchEvent) => {
fetchEvent.respondWith(
caches.match(fetchEvent.request)
.then((response) => response || fetch(fetchEvent.request))
);
};
2
3
4
5
6
- 通用后备
应用程序需要考虑缓存和网络都不可用的情况。
self.onfetch = (fetchEvent) => {
fetchEvent.respondWith(
// 开始执行“从缓存返回,以网络为后备”策略
caches.match(fetchEvent.request)
.then((response) => response || fetch(fetchEvent.request))
.catch(() => caches.match('/fallback.html'))
);
};
2
3
4
5
6
7
8
# 推送通知
要模拟原生应用程序的 Web 应用程序,就要能够接收服务器的推送事件,然后在设备上显示通知。
- 显示通知
服务工作者线程可以通过它们的注册对象使用 Notification API。显示通知要求向用户明确地请求授权。
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
Notification.requestPermission()
.then((status) => {
if (status === 'granted') {
registration.showNotification('foo');
}
});
});
2
3
4
5
6
7
8
9
也可以使用全局 registration 属性触发通知:
self.onactivate = () => self.registration.showNotification('bar');
- 处理通知事件
self.onnotificationclick = ({notification}) => {
console.log('notification click', notification);
clients.openWindow('https://foo.com');
};
self.onnotificationclose = ({notification}) => {
console.log('notification close', notification);
};
2
3
4
5
6
7
- 订阅推送事件
使用 ServiceWorkerRegistration.pushManager 订阅推送消息的例子:
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
registration.pushManager.subscribe({
applicationServerKey: key, // 来自服务器的公钥
userVisibleOnly: true
});
});
2
3
4
5
6
7
也可以使用全局的 registration 属性自己订阅:
self.onactivate = () => {
self.registration.pushManager.subscribe({
applicationServerKey: key, // 来自服务器的公钥
userVisibleOnly: true
});
};
2
3
4
5
6
- 处理推送事件
订阅之后,服务工作者线程会在每次服务器推送消息时收到 push 事件。
self.onpush = (pushEvent) => {
console.log('Service worker was pushed data:', pushEvent.data.text());
};
2
3
结合订阅和推送消息:
main.js
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
// 请求显示通知的授权
Notification.requestPermission()
.then((status) => {
if (status === 'granted') {
// 如果获得授权,只订阅推送消息
registration.pushManager.subscribe({
applicationServerKey: key, // 来自服务器的公钥
userVisibleOnly: true
});
}
});
});
ServiceWorker.js
// 收到推送事件后,在通知中以文本形式显示数据
self.onpush = (pushEvent) => {
// 保持服务工作者线程活动到通知期约解决
pushEvent.waitUntil(
self.registration.showNotification(pushEvent.data.text())
);
};
// 如果用户单击通知,则打开相应的应用程序页面
self.onnotificationclick = ({notification}) => {
clients.openWindow('https://example.com/clicked-notification');
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28