JavaScript 工作者线程学习笔记

工作者线程

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

工作者线程简介

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文件

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对象,但是与之相关的工作者线程可能还没创建,因为存在请求脚本的网络延迟和初始化延迟

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

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

//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在行内脚本创建;这样子没有网络延迟,可以更快的初始化工作者线程

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

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

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

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

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

importScripts('./scripta.js');
importScripts('./scriptb.js');
//or
importScripts('./scripta.js', './scriptb.js');

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

委托任务到子工作者线程

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

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

// 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对象上设置错误事件监听器可以访问到

// 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通信,需要把一个端口传到工作者线程中去

// 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相互之间发送和接收消息

// 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、创建共享工作者线程

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

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

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只能缩小脚本路径,例如:

navigator.serviceWorker.register('/foo/hello.js', {scope: '/'});//这样会抛出错误navigator.serviceWorker.register('/hello.js', {scope: '/foo'});navigator.serviceWorker.register('/hello.js', {scope: '/'});//以上两者不会有问题

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

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对象通过期约返回

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

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

caches.open('v1')	.then(() => caches.has('v1'))	.then(console.log);	//truecaches.open('v1')	.then(() => caches.delete('v1'))	.then(() => caches.has('v1'))	.then(console.log);	//falsecaches.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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Samuel_luo。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值