Skip to content

工作者线程

价值:允许把主线程的工作转嫁给独立的实体,而不会改变现有的单线程模型

工作者线程简介

JavaScript 实际上是运行在托管操作系统中的虚拟环境;浏览器中每打开一个界面,就会分配一个它自己的环境;每个页面就相当于一个沙盒,不会干扰其它界面;所有的这些环境都是并行执行的

使用工作者线程,浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境,这个子环境不能与依赖单线程交互的 API(如 DOM)互操作,但可以与父环境并行执行代码

工作者线程与线程

工作者线程与线程有一些相似之处:

​ 工作者线程是以实际线程实现的

​ 工作者线程并行执行

​ 工作者线程可以共享某些内存:工作者线程能够使用 SharedArrayBuffer 在多个环境间共享内容,使用 Atomic 接口实现并发控制

不同之处:

​ 工作者线程不共享全部内存

​ 工作者线程不一定在同一个进程里

​ 创建工作者线程的开销更大

工作者线程相对比较重,不建议大量使用;工作者线程应该是长期运行的,启动成本比较高,每个实例占用的内存也比较大

工作者线程的类型

有三种类型:专用工作者线程、共享工作者线程、服务工作者线程

1、专用工作者线程

通常简称为工作者线程、Web Worker 或 Worker,可以让脚本单独创建一个 JavaScript 线程,以执行委托任务;只能被创建它的页面使用

2、共享工作者线程

与专用工作者线程很相似,就是可以被多个不同的上下文使用,包括不同的页面;例如同源的脚本就可以向共享工作者线程发送消息或从中接收消息

3、服务工作者线程

它的主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色

WorkerGlobalScope

在工作者线程内部,灭有 window 的概念;这里的全局对象是 WorkerGlobalScope 的实例,通过 self 关键字暴露出来

1、WorkerGlobalScope 属性和方法

self 上可用的属性是 window 对象上属性的严格子集;其中有些属性会返回特定于工作者线程的版本

相关属性查阅红宝书 p793

2、WorkerGlobalScope 子类

每种类型的工作者线程都使用了自己特定的全局对象,继承自 WorkerGlobalScope

专用工作者线程:DedicatedWorkerGlobalScope

共享工作者线程:SharedWorkerGlobalScope

服务工作者线程:ServiceWorkerGlobalScope

专用工作者线程

最简单的 Web 工作者线程,网页中的脚本可以创建专用工作者线程来执行在页面线程之外的其他任务;这样的线程可以与父页面交换信息,发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现不适合在页面线程里做的任务

专用工作者线程的基本概念

可以把专用工作者线程称为后台脚本;一个线程总是从一个脚本源开始

1、创建专用工作者线程

创建专用工作者线程最常见的方式是加载 JavaScript 文件

javascript
const worker = new Worker(location.href + "emptyWorker.js");
//还可以使用相对路径
const worker = new Worker("./emptyWorker.js");
2、工作者线程安全限制

工作者线程的脚本文件只能从与父页面相同的源加载;从其他源加载工作者线程的脚本文件会导致错误

不能使用非同源脚本创建工作者线程,并不影响执行其他源的脚本;使用 importScripts()可以加载其他源的脚本

基于加载脚本创建的工作者线程不受文档的内容安全策略限制,因为工作者线程在与父文档不同的上下文中运行

如果工作者线程加载的脚本带有全局唯一标识符,就会受父文档内容安全策略的限制

3、Worker 对象

Worker()构造函数返回的 Worker 对象是与刚创建的专用工作者线程通信的连接点

在终止工作者线程之前,它不会被垃圾回收,也不能通过编程方式恢复对之前 Worker 对象的引用

Worker 对象支持的事件处理程序属性:onerror、onmessage、onmessageerror(详情查阅红宝书 p795)

还支持下列方法:

​ postMessage():用于通过异步消息事件向工作者线程发送信息

​ terminate():用于立即终止工作者线程;没有为工作者线程提供清理的机会,脚本会突然停止

4、DedicatedWorkerGlobalScope

在专用工作者线程内部,全局作用域是 DedicatedWorkerGlobalScope 的实例;继承自 WorkerGlobalScope,所以包含它的所有属性和方法,工作者线程可以通过 self 关键字访问该全局作用域

工作者线程具有不可忽略的启动延迟

工作者线程和主线程中的 console 都是同一个对象,浏览器会按照自己认为合适的顺序输出这些消息

DedicatedWorkerGlobalScope 在 WorkerGlobalScope 基础上增加了以下属性和方法:

​ name:可以提供 Worker 构造函数的一个可选字符串标识符

​ postMessage():与 worker.postMessage()对应的方法,用于从工作者线程内部向父上下文发送消息

​ close():与 worker.terminate()对应的方法,用于立即终止工作者线程;脚本会突然停止,没有为工作者线程提供清理的机会

​ importScripts():用于向工作者线程中导入任意数量的脚本

专用工作者线程与隐式 MessagePorts

专用工作者线程的 Worker 对象和 DedicatedWorkerGlobalScope 与 MessagePorts 有一些相同接口处理程序和方法:onmessage、onmessageerror、close()、postMessage()

专用工作者线程隐式使用了 MessagePorts 在两个上下文之间通信

父级上下文 Worker 对象和 DedicatedWorkerGlobalScope 实际上融合了 MessagePort,并在自己的接口中分别暴露了相应的处理程序和方法;实际上消息还是通过 MessagePort 发送

专用工作者线程的生命周期

调用 Worker()构造函数是一个专用工作者线程生命的起点;它会初始化工作者线程脚本请求,并把 Worker 对象返回给父上下文;虽然父上下文中可以立即使用这个 Worker 对象,但是与之相关的工作者线程可能还没创建,因为存在请求脚本的网络延迟和初始化延迟

专用工作者线程可以非正式区分为处于下列三个状态:初始化、活动、终止;这几个状态对其他上下文是不可见的;父级上下文无法区分专用工作者线程处于哪种状态

可以把要发送给工作者线程的消息加入队列,这些消息会等待工作者线程状态转变为活动,再把消息添加到它的消息队列

javascript
//init.js
self.addEventListener("message", ({ data }) => console.log(data));
//main.js
const worker = new Worker("./init.js");
worker.postMessage("foo");

// foo

工作者线程只能自我终止(self.close())或者通过外部终止(worker.terminate());只要工作者线程未终止,与之关联的 worker 对象就不会被回收

虽然自我终止时调用了 close,但是工作者线程不会立即停止,而是会取消事件循环中的所有任务,并阻止继续添加新任务;工作者线程不需要执行同步停止

但是外部终止调用 terminate,工作者线程的消息队列就会被清理并锁住

close 和 terminate 是幂等操作,它们只是将 Worker 标记为 teardown,多次调用不会有不好的影响

一个专用工作者线程只会关联一个网页(文档);除了终止,不然只要网页存在,专用工作者线程就会存在;如果浏览器离开网页(关闭标签或关闭窗口),专用工作者线程会被终止

配置 Worker 选项

可以传入一个可选的第二个参数给 Worker 构造函数,该对象支持以下属性:

​ name:可以在工作者线程中通过 self.name 取得字符串标识

​ type:表示加载脚本的运行方式(“classic”或“module”)

​ credentials:在 type 为“module”时,指定如何获取与传输凭证数据相关的工作者线程模块脚本;值有:“omit”、“same-origin”、“include”,这些选项与 fetch()的凭证选项相同;type 为“classic”时默认为“omit”

在 JavaScript 行内创建工作作者线程

专用工作者线程可以通过 Blob 对象 URL 在行内脚本创建;这样子没有网络延迟,可以更快的初始化工作者线程

javascript
const worker = new Worker(URL.createObjectURL(new Blob([`self.onmessage = ({data}) => console.log(data);`])));
worker.postMessage("blob worker");
//blob worker

这样在工作者线程中向父级发送消息的函数中不能包含父级中的闭包引用,比如 window

在工作者线程中动态执行脚本

工作者线程中可以使用 importScripts()来动态加载任意脚本;该方法可用于全局 Worker 对象,所以可以在脚本顶级直接使用;这个方法会加载脚本呢并且按照加载顺序同步执行

浏览器下载时顺序没有限制,但是执行时会严格按照它们在参数列表的顺序执行

javascript
importScripts("./scripta.js");
importScripts("./scriptb.js");
//or
importScripts("./scripta.js", "./scriptb.js");

工作者线程内部可以请求任何源的脚本,不会受到 CORS 的限制

委托任务到子工作者线程

有时候需要在工作者线程中再创建子工作者线程;再有多个 CPU 时,这样可以事现并行运算;但是要考虑多个子线程会有很大计算成本

子工作者线程的脚本路径根据父工作者线程而不是相对于网页来解析

javascript
// main.js
const worker = new Worker("./js/worker.js");
// js/worker.js
const worker = new Worker("./subworker.js");
// js/subworker.js
console.log("hello");

//hello

所有工作者线程的脚本都要和主页相同的源加载

处理工作者线程错误

如果工作者线程脚本出现了错误,该工作者线程沙盒可以阻止它打断父线程的执行,所以在父线程中 try/catch 方法捕捉不到工作者线程中的错误;而是可以通过在 Worker 对象上设置错误事件监听器可以访问到

javascript
// main.js
const worker = new Worker("./worker.js");
worker.onerror = console.log;
// worker.js
throw Error("hello");

与专用工作者线程通信

与工作者线程通信都是通过异步消息完成的

1、使用 postMessage()

该方法可以传递序列化信息

Window.prototype.postMessage 有 targetOrigin 限制;但是 WorkerGlobalScope.prototype.postMessage 和 Worker.prototype.postMessage 没有,因为工作者线程脚本的源被限制为主页的源,所以没必要再过滤了

2、使用 MessageChannel

Channel Message API 可以在两个上下文间明确建立通讯渠道

MessageChannel 实例有两个端口,分别代表两个通讯端点;要让父页面和工作线程通过 MessageChannel 通信,需要把一个端口传到工作者线程中去

javascript
// worker.js
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 }) => {
      messagePort.postMessage(`${data}! = ${factorial(data)}`);
    };
  }
};

// main.js
const channel = new MessageChannel();
const factorialWorker = new Worker("./worker.js");
factorialWorker.postMessage(null, [channel.port1]);
channel.port2.onmessage = ({ data }) => console.log(data);
channel.port2.postMessage(5);

// 5! = 120

使用数组传递端口是为了在两个上下文中传递可转移对象(Transferable)

使用 MessageChannel 真正有用的地方是让两个工作者线程之间直接通信,这个可以通过把端口传给另一个工作者线程实现

3、使用 BroadcastChannel

同源脚本能够通过 BroadcastChannel 相互之间发送和接收消息

javascript
// main.js
const channel = new BroadcastChannel("worker_channel");
const worker = new Worker("./worker.js");
channel.onmessage = ({ data }) => {
  console.log(data);
};
setTimeout(() => channel.postMessage("foo"), 1000);

// worker.js
const channel = new BroadcastChannel("worker_channel");
channel.onmessage = ({ data }) => {
  concole.log(data);
  channel.postMessage("bar");
};

这种信道没有端口所有权的概念,所以如果没有实体监听这个信道,广播的消息就不会有人处理;所以这里需要使用 setTimeout 延迟一秒

工作者线程传输数据

在使用工作者线程时,经常需要为它们提供某种形式的数据负载

在 JavaScript 中,有三种在上下文间转移信息的方式:结构化克隆算法、可转移对象、共享数组缓冲

1、结构化克隆算法

结构化克隆算法可用于在两个独立上下文间共享数据;该算法由浏览器后台实现,不能直接调用

在通过 postMessage()传递对象时,浏览器会遍历该对象,并在目标上下文中生成它的一个副本

传给 postMessage()第一个参数是要发送到其他窗口的数据,它将会被结构化克隆算法序列化;这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化

该算法支持类型有:除 Symbol 之外的所有原始类型、Boolean 对象、String 对象、BDate、RegExp、Blob、File、FileList、ArrayBuffer、ArrayBufferView、ImageData、Array、Object、Map、Set

有以下几点要注意:

​ 复制之后,源上下文中对该对象的修改,不会传播到目标上下文中的对象

​ 该算法可以识别对象中包含的循环引用,不会无穷遍历对象

​ 克隆 Error 对象、Function 对象或 DOM 节点会抛出错误

​ 该算法并不总是创建完全一致的副本

​ 对象属性描述符、获取方法和设置方法不会克隆,必须要使用默认值

​ 原型链不会克隆

​ RegExp.prototype.lastIndex 属性不会克隆

结构化克隆算法在对象比较复杂时会存在计算机性消耗,所以要尽可能避免过大、过多的复制

2、可转移对象

使用可转移对象可以把所有权从上一个上下文转移到另一个上下文

可转移对象为:ArrayBuffer、MessagePort、ImageBitmap、OffscreenCanvas

postMessage()方法第二个可选参数是数组,它指定应该将哪些对象转移到目标上下文;原来的上下文将不会再拥有该对象的所有权,会从其上下文抹去

其它类型的对象中嵌套可转移对象也完全没有问题;包装对象会被复制,而嵌套对象会被转移

3、SharedArraybuffer

由于 Spectre 和 Meltdown 的漏洞,所有主流浏览器在 2018 年就禁用了它;2019 年开始才有浏览器逐步恢复

它既不克隆也不转移,SharedArrayBuffer 作为 ArrayBuffer 能够在不同浏览器上下文间共享

再把 SharedArrayBuffer 传给 postMessage()时,浏览器只会传递原始缓冲区的引用;两个不同的 js 上下文分别会维护同一个内存块的引用

在两个并行线程中共享内存块有资源竞争的风险;SharedArrayBuffer 实例,实际上会被当成异变(volatile)内存

可以使用 Atomics 对象,让一个工作者线程获得 SharedArrayBuffer 实例的锁

线程池

因为启用工作者线程代价很大,所以某些情况下可以考虑始终保持固定数量的线程活动,需要时就把任务分派给它们;工作者线程再执行计算时,会被标记为忙碌状态;直到通知线程池自己空闲了才准备好接收新任务;这些活动线程就称为”线程池“或“工作者线程池”

可以根据 navigator.hardware Concurrency 属性返回系统可用的核心数量来确定创建线程池中线程数量的上限

一般通过使用特定于任务的线程池,可以分配固定数量的工作者线程,并根据需要为它们提供参数;工作者线程接收参数,进行计算,最后返回结果给线程池;线程池可以再将其它任务分配给它

我们可以创建一个简单的线程池;首先是定义一个 TaskWorker 类,它可以扩展 Worker 类;TaskWorker 负责两件事:跟踪线程是否正忙于工作,并管理进出线程的信息与事件;另外,传入给这个工作者线程的任务会封装到一个期约中,然后正确地解决和拒绝

相关代码实例查阅红宝书 p810

共享工作者线程

共享工作者线程或共享线程与专用工作者线程类似,但可以被多个可信任的执行上下文访问;共享工作者线程的消息接口稍有不同,包括外部和内部

共享工作者线程简介

可以看作是专用工作者线程的一个扩展;线程创建、线程选项、安全限制、importScript()行为都是相同的

1、创建共享工作者线程

与专用工作者线程创建一样

javascript
const sharedWorker = new SharedWorker("./worker.js");
const sharedWorker = new SharedWorker("./worker.js", { name: "a" });

可以在行内脚本创建共享工作者线程,但是这样没有什么意义;因为每个基于行内脚本字符串创建的 Blob 都会被赋予自己唯一的浏览器内部 URL,所以行内脚本中创建的工作者线程始终是唯一的

2、SharedWorker 标识与独占

SharedWorker()只会在相同标识不存在的情况下创建新实例;如果标识相同则与原有的共享者线程创建新的连接

共享者线程的标识源自解析后的脚本 URL、工作者线程名称和文档源

3、使用 SharedWorker 对象

构造函数返回的 SharedWorker 对象被用作与新创建的共享工作者线程通信的连接点;它可以用来通过 MessagePort 在共享工作者线程和父上下文间传递信息,也可以用来捕获共享线程中发出的错误事件

SharedWorker 对象支持以下属性:

​ onerror:在共享线程中发生 ErrorEvent 类型的错误事件时会调用指定给该属性的处理程序

​ 此事件会在共享线程抛出错误时发生;也可以使用 DOM2 方式添加

​ port:专门用来跟共享线程通信的 MessagePort

4、SharedWorkerGlobalScope

共享线程内部,全局作用域是 SharedWorkerGlobalScope,继承自 WorkerGlobalScope,因此包括它的所有属性和方法;共享线程也可以通过 self 关键字访问该全局上下文

它对 WorkerGlobalScope 进行了下列扩展:

​ name:可选的字符串标识符,可以传给构造函数

​ importScript()

​ close():与 worker.terminate 对应,用于立即终止工作线程;不会给工作线程提供终止前清理的机会;脚本会突然终止

​ onconnect:与共享线程创建新连接时,应将其设置为处理程序;connect 事件包括 MessagePort 实例的 ports 数组,可用于把消息发送回上下文

​ 通过 worker.port.onmessage 或 worker.port.start()与共享线程简历连接时都会触发 connect 事件;connect 事件也可以通过使用 DOM2 方式添加

理解共享工作者线程的生命周期

共享工作者线程只要还有一个上下文连接就会持续存在;浏览器会记录连接总数,在连接数为 0 时,线程终止

没有办法以编程方式终止共享线程;在共享端口调用 close 方法时,只要还有一个端口连接到该线程,就不会真正的终止线程

SharedWorker 的“连接”与关联 MessagePort 或 MessageChannel 状态无关;只要建立了连接,浏览器会负责管理该连接

只有当前页面销毁且没有连接时,浏览器才会终止共享线程

连接到共享工作者线程

每次调用构造函数,无论是否创建了工作者线程,都会在共享线程内部触发 connect 事件

发生 connect 事件时,SharedWorker()构造函数会隐式创建 MessagePort 实例,并把它的所有权唯一的转移给 SharedWorker 的实例;这个 MessagePort 实例会保存在 connect 事件对象的 ports 数组中

一个连接事件代表一个连接,一个连接事件发生时共享线程会收到一个 MessagePort 实例,多次请求连接事件,会导致端口越来越多,ports 集合还会可能受到死端口的污染,没办法识别它们;有一个办法是在 beforeunload 事件即将销毁页面时,发送卸载消息给共享线程,让共享线程有机会清除死端口

服务工作者线程

该线程类似于浏览器中代理服务器的线程,可以拦截外出请求和缓存响应;可以让网页在没有网络连接的情况下也能正常使用,因为部分或全部页面可以从服务工作者线程缓存中提供服务;该线程可以使用 Notifications API、Push API、Background Sync API、Channel Messaging API

来自一个域的多个页面共享一个服务工作者线程;不过为了使用 Push API 等特性,服务工作者线程也可以在相关标签页或浏览器关闭后继续等待到来的推送事件

该线程在两个主要任务上最有用:充当网络请求的缓存层、启用推送通知

服务工作者线程

1、ServiceWorkerContainer

服务工作者线程与另外两个线程的一个区别是没有全局构造函数;服务工作者线程是通过 ServiceWorkerContainer 来管理的,它的实例保存在 navigator.serviceWorker 属性中;该对象是个顶级接口,通过它可以让浏览器创建、更新、销毁、与服务工作者线程交互

2、创建服务工作者线程

与共享线程类似,服务工作者线程同样是不存在时创建新实例,存在时连接已有实例

服务工作者线程没有通过全局构造函数创建,而是暴露了 register()方法,同样的接收一个脚本 URL

javascript
navigator.serviceWorker.register("./emptyServiceWorker.js");

register()方法返回一个期约,解决为一个 ServiceWorkerRegistration 对象,失败时拒绝

同一页面重复的 URL 调用 register()时什么也不执行

一般服务工作者线程会在 load 事件中创建,因为线程的创建会与页面资源的加载重叠,进而拖慢初始页面渲染的过程(如果该服务工作者线程负责管理缓存,这样就需要尽早注册)

3、使用 ServiceWorkerContainer 对象

该对象始终可以在客户端上下文中访问

该对象支持以下事件处理程序:

​ oncontrollerchange:在 ServiceWorkerContainer 触发 controllerchange 事件时会调用指定的事件处理程序

​ 此事件在获得新激活的 ServiceWorkerRegistration 时触发;可以使用 DOM2 方式添加处理程序

​ onerror:在关联的服务工作者线程触发 ErrorEvent 错误事件时会调用指定的事件处理程序

​ 在关联的服务工作者线程内部抛出错误时触发;可以使用 DOM2 方式添加处理程序

​ onmessage:在服务工作者线程触发 MessageEvent 事件时会调用指定的事件处理程序

​ 在服务脚本向父级上下文发送消息时触发;可以使用 DOM2 方式添加事件处理程序

该对象支持以下属性:

​ ready:返回期约,解决为激活的 ServiceWorkerRegistration 对象;该期约不会拒绝

​ controller:返回与当前页面关联的激活的 ServiceWorker 对象,如果没有激活的线程则返回 null

该对象支持下列方法:

​ register():使用接收的 URL 和 options 对象创建或更新 ServiceWorkerRegistration

​ getRegistration():返回期约,解决为与提供的作用域匹配的 ServiceWorkerRegistration 对象;没有匹配则返回 undefined

​ getRegistrations():返回期约,解决为与 ServiceWorkerContainer 关联的 ServiceWorkerRegistration 对象的数组;如果没有关联的服务线程则返回空数组

​ startMessage():开始传送通过 Client.postMessage()派发的消息

4、使用 ServiceWorkerRegistration 对象

该对象表示注册成功的服务工作者线程;可以在 register()方法返回的解决期约的处理程序中获取到

同一页面使用同一 URL 多次调用该方法会返回相同的注册对象

该对象支持以下处理程序:

​ onupdatefound:在服务工作者线程触发 updatefound 事件会调用指定的事件处理程序

​ 此事件会在服务工作者线程开始安装新版本时触发,表现为 ServiceWorkerRegistration.installing 收到一个新的服务工作者线程;可以使用 DOM2 方法添加事件处理程序

该对象支持以下通用属性:

​ scope:返回线程作用域的完整 URL 路径

​ navigationPreload:返回与注册对象关联的 NavigationPreloadManager 实例

​ pushManager:返回与注册对象关联的 pushManager 实例

该对象还支持以下属性:

​ installing:如果有则返回状态为 installing(安装)的服务工作者线程,否则为 null

​ waiting:如果有则返回 waiting(等待)的服务工作者线程,否则为 null

​ active:如果有则返回状态 activating 或 active(活动)的服务工作者线程,否则为 null

这些属性都是服务工作者线程状态的一次快照;活动状态的服务工作者线程在页面的生命周期内不会改变状态,除非强制这样做

该对象还支持以下方法:

​ getNotifications():返回期约,解决为 Notification 对象的数组

​ showNotifications():显示通知,可以配置 title 和 options 参数

​ update():直接从服务器重新请求服务脚本,如果脚本不同,则重新初始化

​ unregister():取消服务工作者线程的注册;该方法会在服务工作者线程执行完再取消注册

5、使用 ServiceWorker 对象

该对象可以通过 ServiceWorkerContainer 对象的 controller 属性和通过 ServiceWorkerRegistration 的 active 属性获得,该对象继承 Worker 对象,所以包括其所有属性和方法,除了 terminate()方法

该对象支持以下事件处理程序:

​ onstatechange:在 ServiceWorker.state 变化时发生,可以使用 DOM2 方法添加事件处理程序

​ scriptURL:解析后注册服务工作者线程的 URL

​ state:表示服务工作者线程状态的字符串,可能的值:installing、installed、activating、activated、redundant

6、服务工作者线程的安全限制

服务工作者线程也受到加载脚本对应源的常规限制;服务工作者线程 API 只能在安全上下文(HTTPS)下使用;非安全上下文中,navigator.serviceWorker 是 undefined;为方便开发,浏览器豁免了通过 localhost 或 127.0.0.1 在本地加载页面的安全上下文规则

通过 window.isSecureContext 确定上下文是否安全

7、ServiceWorkerGlobalScope

服务工作者线程内部,全局上下文是 ServiceWorkerGlobalScope 的实例;该对象继承自 WorkerGlobalScope,因此拥有它的所有属性和方法;服务工作者线程可以通过 self 关键字访问全局上下文

该对象扩展了 WorkerGlobalScope 属性:

​ caches:返回服务工作者线程的 CacheStorage 对象

​ clients:返回线程的 Clients 接口,用于访问底层 Client 对象

​ registration:返回线程的 ServiceWorkerRegistration 对象

​ skipWaiting():强制线程进入活动状态;需要和 Client.claim()一起使用

​ fetch():在线程内部发送常规网络请求

服务工作者线程不像另外两线程一样,它可以接收很多事件,包括页面操作、通知操作触发的事件或推送事件

在服务工作者线程中打印日志到控制台不一定能在浏览器默认控制台看见

服务工作者线程的全局作用域可以监听以下事件:

​ 线程状态:

​ install:客户端可以通过 ServiceWorkerRegistration.installing 判断或者 self.oninstall 属性上指定事件处理程序(这是线程接收的第一个事件,在线程一开始就会触发;只会调用一次)

​ activate:客户端可以通过 ServiceWorkerRegistration.active 判断或者 self.onactive 属性上指定事件处理程序(线程准备好处理功能性事件和控制客户端时触发;该事件只表明具有控制客户端的条件)

​ Fetch API

​ fetch:在线程截获来自主页面 fetch()请求时触发;服务工作者线程的 fetch 事件处理程序可以访问 FetchEvent,可以根据需要调整输出;可以在 self.onfetch 属性指定事件处理程序

​ Message API

​ message:线程通过 postMessage()获取数据时触发,可以在 self.onmessage 属性上指定事件处理程序

​ Notification API

​ notificationclick:在系统告诉浏览器用户点击了 ServiceWorkerRegistration.showNotification()生成的通知时触发,也可以在 self.onnotificationclick 属性上指定该事件的处理程序

​ notificationclose:在系统告诉浏览器用户关闭或取消显示了 ServiceWorkerRegistration.showNotification()生成的通知时触发,也可以在 self.onnotificationclose 属性上指定事件处理程序

​ Push API

​ push:在线程接收到推动消息时触发,也可以在 self.onpush 属性上指定事件处理程序

​ pushsubscriptionchange:在应用控制外因素导致推送状态变化时触发,也可以在 self.onpushsubscriptionchange 属性上指定该事件的处理程序

8、服务工作者线程作用域限制

服务工作者线程只能拦截其作用域内客户端发送的请求;作用域是相对于获取服务脚本的路径定义的

创建线程时,如果在 options 对象中规定了 scope,则 scope 只能缩小脚本路径,例如:

javascript
navigator.serviceWorker.register("/foo/hello.js", { scope: "/" });
//这样会抛出错误

navigator.serviceWorker.register("/hello.js", { scope: "/foo" });
navigator.serviceWorker.register("/hello.js", { scope: "/" });
//以上两者不会有问题

一般使用末尾带斜杠的绝对路径来定义:

javascript
navigator.serviceWorker.register("/hello.js", { scope: "/foo/" });

这样有两个好处:将脚本文件的相对路径与作用域的相对路径分开,同时将该路径本身排出在作用域之外

如果想扩展服务工作者线程的作用域,有两种方式:

​ 通过包含想要的作用域的路径提供(获取)脚本

​ 给服务脚本的响应添加 Service-Worker-Allowed 头部,把他的值设置为想要的作用域,该作用域值因该要与 register()中的作用域值一致

服务工作者线程缓存

服务工作者线程一个主要能力是可以通过编程方式实现真正的网络请求缓存机制,该缓存有几点特点:

​ 缓存不自动缓存任何请求;所有缓存必须明确指定

​ 缓存没有到期失效的概念;除非明确删除

​ 缓存必须手动更新和删除

​ 缓存版本必须手动清理;每次线程更新,新线程负责提供新的缓存键以保存新缓存

​ 唯一的浏览器强制逐出策略基于服务工作者线程缓存占用的空间;线程负责管理自己缓存占用的空间,缓存超限时,浏览器会基于最近最少使用原则为新缓存腾出空间

本质上服务工作者线程缓存机制是一个双层字典,顶级字典条目映射到二级嵌套字典;顶级字典是 CacheStorage 对象,可通过线程全局作用域的 caches 属性访问;顶级字典的每个值都是一个 Cache 对象,该对象也是一个字典,是 Request 对象到 Response 对象的映射

CacheStorage 中的 Cache 条目只能以源为基础存取

CacheStorage 和 Cache 对象在主页面或其它工作者线程中也能使用

1、CacheStorage 对象

CacheStorage 对象是映射到 Cache 对象的字符串键值对;其提供的 API 类似于异步 Map;其接口通过全局对象 caches 属性暴露出来

cache.open()方法会取得 CacheStorage 中的缓存,传入字符串(非字符串会被转换为字符串);缓存不存在则会创建

Cache 对象通过期约返回

javascript
caches.open("v1").then(console.log);
//Cache {}

CacheStorage 也有类似 Map 中的 has()、delete()、keys()方法,但是都基于期约

javascript
caches
  .open("v1")
  .then(() => caches.has("v1"))
  .then(console.log); //true

caches
  .open("v1")
  .then(() => caches.delete("v1"))
  .then(() => caches.has("v1"))
  .then(console.log); //false

caches
  .open("v1")
  .then(() => caches.keys())
  .then(console.log); //["v1", "v2"]

CacheStorage 接口还有一个 match()方法,可以根据 Request 对象搜索 CacheStorage 中的所有 Cache 对象;搜索顺序是 keys()的顺序,返回匹配的第一个响应

2、Cache 对象

CacheStorage 通过字符串映射到 Cache 对象;Cache 对象类似于异步的 Map;Cache 键可以是 URL 字符串,也可以是 Request 对象;这些键会映射到 Response 对象

服务工作者线程只会考虑缓存 HTTP 的 GET 请求;默认情况下,Cache 不允许使用 POST、DELETE、PUT 等请求方法(这些方法会与服务器动态交换信息,因此不适合客户端缓存)

有三个方法可以填充 Cache:

​ put(request, response):在键和值同时存在时用于添加缓存项;该方法返回期约,添加成功后会解决

​ add(request):在只有 Request 对象或 URL 时使用此方法发送 fetch 请求,并缓存响应;该方法返回期约;期约添加成功后会解决

​ addAll(requests):在希望填充全部缓存时使用;该方法接收 URL 或 Request 对象的数组,对数组的每一项调用 add 方法;该方法返回期约,期约会在所有缓存内容成功添加后会解决

Cache 也有 delete()和 keys()方法,这些方法与 Map 类似,但都基于期约

要检索 Cache,有两个方法:

​ matchAll(request, options):返回期约,解决为匹配缓存中 Response 对象的数组

​ 可以对结构相似的缓存执行批量操作;通过 options 对象配置请求匹配方式

​ match(request, options):返回期约,解决为匹配缓存中的 Response 对象;未命中则返回 undefined

​ 相当于 matchAll(request, options)[0];通过 options 对象配置请求匹配方式

缓存是否命中取决于 URL 字符串或 Request 对象 URL 是否匹配;URL 字符串和 Request 对象是可互换的,因为匹配时会提取 Reqeust 对象的 URL

Cache 对象使用 Request 和 Response 对象的 clone()方法创建副本,并储存为键值对;所以 Cache 缓存中取得的实例并不等于原始的键值对

options 对象可以通过设置以下属性来配置 URL 匹配行为:

​ cacheName:只有 CacheStorage.matchAll()支持;设置为字符串时,只会匹配 Cache 键为指定字符串的缓存值

​ ignoreSearch:设置为 true 时,在匹配 URL 时忽略查询字符串,包括请求查询和缓存键

​ ignoreMethod:设置为 true 时,在匹配 URL 时忽略请求查询的 HTTP 方法

​ ignoreVary:匹配的时候考虑 HTTP 的 Vary 头部,该头部指定哪个请求头部导致服务器响应的不同值;设置为 true 时,匹配 URL 时忽略 Vary 头部

3、最大储存空间

浏览器需要限制缓存占用的磁盘空间,否则无限制存储势必会造成滥用;该部分没有任何规定,都由浏览器供应商决定

使用 StorageEstimate API 可以近似获悉有多少空间可用,以及当前使用了多少空间,此方法只在安全的上下文中可用(输出的数值可能不正确)

服务工作者线程客户端

服务工作者线程会使用 Client 对象跟踪关联的窗口、工作线程或服务工作者线程;服务工作者线程可以通过 Clients 接口访问这些 Client 对象;该接口暴露在全局上下文的 self.client 属性上

Client 对象支持以下属性和方法:

​ id:返回客户端的全局唯一标识符;id 可用于通过 Client.get()获取客户端的引用

​ type:返回标识客户端类型的字符串(可能是 window、worker、sharedworker)

​ url:返回客户端的 URL

​ postMessage():用于向单个客户端发送消息

Clients 接口支持通过 get()或 matchAll()访问 Client 对象;这两个方法都通过期约返回结果

matchAll()也可以接收 options 对象,该对象支持以下属性:

​ includeUncontrolled:在设置为 true 时,返回的结果包含不受当前服务工作者线程的控制的客户端;默认 false

​ type:可以设置为 window、worker、sharedworker,对返回结果进行过滤;默认 all,返回所有类型客户端

Clients 接口也支持以下方法:

​ openWindow(url):在新窗口中打开指定 URL,实际上会给当前服务工作者线程添加一个新 Client;这个新 Client 对象以解决的期约形式返回

​ claim():强制性设置当前服务工作者线程以控制其作用域中的所有客户端

服务工作者线程与一致性

服务工作者线程最终的用途是:让网页能够模拟原生应用程序;要像原生应用程序一样,服务工作者线程必须支持版本控制

从全局角度说,服务工作者线程的版本控制可以确保任何时候两个网页的操作都有一致性,主要表现为:

代码一致性,服务工作者线程为此提供了一种强制机制,确保来自同源的所有并存页面始终会使用来自相同版本的资源

数据一致性,服务工作者线程的资源一致性机制可以保证网页输入/输出行为对同源的所有并存网页都相同

为确保一致性,服务工作者线程的生命周期不遗余力的避免出现有损一致性的现象:

服务工作者线程提早失败,在安装服务工作者线程时,任何预料之外的问题都有可能阻止服务工作者线程的成功安装(服务脚本加载失败、服务脚本语法错误、服务脚本运行时错误、无法通过 importScript()加载工作者线程依赖、某个缓存加载失败)

服务工作者线程激进更新,浏览器再次加载服务脚本时,服务脚本或通过 importScripts()加载的依赖中哪怕有一个子节的差异,也会启动安装新版本的服务工作者线程

未激活服务工作者线程消极活动,当页面上第一次调用 register()时,服务工作者线程会被安装,但不会被激活,并且在导航事件发生前不会控制页面

活动的服务工作者线程粘连,至少有一个客户端与关联到活动的服务工作线程,浏览器就会在该源的所有界面中使用它;创建新工作者线程后,浏览器只会在前一个线程关联的客户端为 0(或者强制更新服务工作者线程)时才会切换到新的工作者线程

理解服务工作者线程的生命周期

Service Worker 规范定义了六种服务工作者线程的状态:已解析(parsed)、安装中(installing)、已安装(installed)、激活中(activating)、已激活(activated)、已失效(redundant)

完整的服务工作者线程会以该顺序进入相应状态,尽管不会进入每个状态

每次状态的变化都会在 ServiceWorker 对象上触发 statechange 事件,可以通过 DOM0 方法添加事件处理程序

1、已解析状态

调用 register 刚创建的服务工作者线程实例会进入已解析状态;该状态没有事件,也没有 ServiceWorker.state 值

浏览器获取脚本文件,然后执行一些初始化任务,服务工作者线程的生命周期就开始了:

​ 确保服务脚本来自相同的源

​ 确保在安全上下文中注册服务工作者线程

​ 确保服务脚本可以被浏览器 JavaScript 解释器成功解析而不会抛出错误

​ 捕获服务脚本的快照

所有这些任务全部成功,register()会返回期约解决为一个 ServiceWorkerRegistration 对象;新服务工作者线程进入安装中状态

2、安装中状态

这个状态是执行所有服务工作者线程设置任务的状态

详细事件查阅红宝书 p831

这个状态频繁用于填充服务工作者线程的缓存,如过任何资源缓存失败,线程都会安装失败并跳至已失效状态

线程可以通过 ExtendableEvent 停留在安装中状态;可以调用 ExtendableEvent.waitUntil(),该方法接收一个期约参数,会将这个状态延迟到期约解决

如果没有错误发生或者没有拒绝,线程就会前进到已安装状态

3、已安装状态

这个状态线程会等到许可后去控制客户端;如果没有别的线程,则该线程跳到这个状态后会直接进入激活中状态

可以在客户端检查 ServiceWorkerRegistration.waiting 是否被设置为一个 ServiceWorker 来确定是否处于这个阶段

已安装状态是触发逻辑的好时机,可以通过 self.skipWaiting()强制推进服务工作者线程的状态,也可以提示用户重新加载应用程序

4、激活中状态

改状态表示线程已经被浏览器选中即将变成可以控制页面服务的工作者线程;如果没有活动中的线程,新线程自动到达激活中状态;如果过有一个活动的线程,则新线程有两种方式进入激活中状态:

​ 原有服务线程控制的客户端为 0

​ 已安装的线程中调用 self.skipWaiting()

激活中状态下,不能执行发送请求或推送事件

可以通过检查 ServiceWorkerRegistration.active 是否被设置为一个 ServieWorker 实例来确定该状态;但是已激活状态也是这个属性,所以不能用这个方式来区分这两个状态

线程内部可以添加 activate 事件来获悉

activate 事件也继承自 ExtendableEvent,所以也支持 waitUntil()方法

5、已激活状态

该状态表示服务工作者线程正在控制一个或多个客户端;这个状态中,线程会捕获其作用域中断 fetch、通知和推送事件

可以通过 ServiceWorkerRegistration.active 来检查该状态,但是不可靠

检查 ServiceWorkerRegistration 的 controller 属性,返回激活的 ServiceWorker 实例更可靠的检查该状态

新线程控制客户端时,客户端 ServiceWorkerContainer 会触发 controllerchange 事件

可以使用 ServiceWorkerContainer.ready 期约来检查活动线程,期约会在当前页面拥有活动的线程时立即解决

6、已失效状态

表示线程已经死亡;不会再有事件发送给他,浏览器随时可能销毁它并回收它的资源

7、更新服务工作者线程

更新的流程初始阶段是更新检查,也就是浏览器重新请求服务脚本;以下事件可以触发:

​ 使用不一样的 URL 调用 register()方法

​ 浏览器导航到线程作用域中的另一个页面

​ 发生了 fetch 或 push 等功能性事件,且至少 24 小时没发生更新检查

新的服务脚本会与当前线程的服务脚本进行比较;如果不同,浏览器会用新脚本初始化一个线程,该线程进入自己的生命周期,直到抵达已安装状态,浏览器会让它安全的获得页面的控制权

取代现有线程的唯一方法是关闭所有受控页面

控制反转与服务工作者线程的持久化

服务工作者线程是没有状态的;服务工作者线程遵循控制反转模式并且是事件驱动的

线程中绝大多数代码应该在处理程序中定义;线程脚本执行次数变化很大,高度依赖浏览器状态,因此脚本的行为应该是幂等的

线程的生命周期与他所控制的客户端生命周期无关

浏览器检测到某个线程空闲了,就可以终止他并在需要时再重新启动

通过 updateViaCache 管理服务文件缓存

正常情况下,浏览器加载的所有 JavaScript 资源会按照它们的 Cache-Control 头部纳入 HTTP 缓存管理;服务脚本没有优先权,浏览器不会在缓存文件失效前接收更新的服务脚本

解决方法是服务端在相应服务脚本时设置 Cache-Control:max-age=0 头部;这样浏览器始终可以取得最新的脚本文件

客户端可以通过 updateViaCache 属性设置客户端对待服务脚本的方式;在注册线程时定义,有三个值:

​ imports:默认值;顶级服务脚本永远不会被缓存,但是 importScript()的脚本会按照 Cache-Control 头部设置纳入 HTTP 缓存

​ all:服务脚本没有任何特殊待遇

​ none:顶级脚本和 importScript()导入的脚本永远不会被缓存

强制性服务工作者线程操作

某些情况下可能需要线程快速进入已激活状态,可以使用 skipWaiting()

浏览器会在每次导航事件中重新检查服务脚本;ServiceWorkerRegistration 对象提供了一个 update()方法来实现这个过程

服务工作者线程消息

线程可以通过 postMessage()与客户端交换消息

发送给服务工作者线程的消息可以在全局作用域中处理,而发送回客户端的消息可以在 ServiceWorkerContext 对象上处理

相关代码查阅红宝书 p836

通过 MessageChannel 或 BroadcastChannel 也可能实现

拦截 fetch 事件

服务工作者线程最重要的特性之一就是拦截网络请求

这种拦截能力不限于 fetch 发送的请求,JavaScript、CSS、图片和 HTML(包括 HTML 文档本身)等资源发送到网络请求

FetchEvent 继承自 ExtendableEvent;让服务工作者线程能够决定如何处理 fetch 事件的方法是 event.respondWith();该方法接收期约,解决为一个 Response 对象;该对象从哪返回的可以自己决定

1、从网络返回

简单的转发 fetch 事件

2、从缓存返回

本质上是缓存检查

3、从网络返回,缓存作后备

把网络返回作为首选,如果缓存中有值就返回缓存中的值

5、通用后备

当缓存和网络都不可用时发生;应该在线程安装时缓存后备资源,然后在这时返回

推送通知

网页要能够接收服务器的推送事件,然后在设备上显示通知(即使应用程序没有运行)

在 PWA 应用程序中支持推送消息,必须支持以下四种行为:

​ 线程必须能显示通知

​ 线程必须能够处理与这些通知的交互

​ 线程必须能够订阅服务器发送的推送消息

​ 线程必须能够处理推送消息,即使应用程序没在前台运行或者打开

1、显示通知

线程可以通过注册对象使用 Notification API;好处:与服务工作者线程相关的通知也会触发服务工作者线程内部的交互事件

显示通知要求明确的向用户请求授权;授权后,可以通过 ServiceWorkerRegistration.showNotification()显示通知

线程内部可以用 registration 属性触发通知

2、处理通知事件

通过 ServiceWorkerRegistration 创建的通知会向线程发送 notificationclick 和 notificationclose 事件

一般来说在线程处理程序中,可以通过 clients.openWindow()打开相应的 URL

详情代码请查阅红宝书 p839

3、订阅推送事件

对于发送给线程的推送消息,必须通过线程的 PushManager 来订阅;这样线程就可以在 push 事件处理程序中推送消息;ServiceWorkerRegistration.pushManager 可以订阅

线程也可以使用全局的 registration 属性自己订阅

相关代码查阅红宝书 p840

4、处理推送事件

订阅之后,线程会在每次服务器推送消息时收到 push 事件

push 事件继承了 ExtendableEvent;可以把 showNotification()返回的期约传给 waitUntil(),这样会让线程一直活动到通知的期约解决

代码查阅红宝书 p840