27.3 共享工作者线程

共享工作者线程或共享线程与专用工作者线程类似,但可以被多个可信任的执行上下文访问。
例如,同源的两个标签页可以访问同一个共享工作者线程。SharedWorker与Worker的消息接口稍有不同,包括外部和内部。
共享线程适合开发者希望通过在多个上下文间共享线程,减少计算性消耗的情形。
比如,可以用一个共享线程管理多个同源页面WebSocket消息的发送与接收。共享线程也可以用在同源上下文希望通过一个线程通信的情形。

27.3.1 共享工作者线程简介

从行为上讲,共享工作者线程可看作是专用工作者线程的一个扩展。
线程创建、线程选项、安全限制和importScripts()的行为都是相同的。
与专用工作者线程一样,共享工作者线程也在独立执行上下文中运行,也只能与其他上下文异步通信。

1.创建共享工作者线程

与专用工作者线程一样,创建共享工作者线程非常常用的方式是:通过加载JavaScript文件创建。此时,需要给SharedWorker构造函数传入文件路径,该构造函数在后台异步加载脚本,并实例化共享工作者线程。

2.SharedWorker标识与独占

共享工作者线程与专用工作者线程的一个重要区别在于:
Worker()构造函数始终会创建新实例;
SharedWorker()则只会在相同的标识不存在的情况下才创建新实例。
如果的确存在与标识匹配的共享工作者线程,则只会与已有共享者线程建立新的连接。
共享工作者线程标识源自解析后的脚本URL、工作者线程名称和文档源。
因为可选的工作者线程名称也是共享工作者线程标识的一部分,所以不同的线程名称,会强制浏览器创建多个共享工作者线程。
共享线程,顾名思义,可以在不同标签页、不同窗口、不同内嵌框架或同源的其他工作者线程之间共享。
初始化共享线程的脚本只会限制URL。

3.使用SharedWorker对象

SharedWorker()构造函数返回的SharedWorker对象,被用作与新创建的共享工作者线程通信的连接点。它可以用来通过MessagePort在共享工作者线程和父上下文间传递信息,也可以用来捕获共享线程中发出的错误事件。
SharedWorker对象支持以下属性:
❑ onerror:在共享线程中发生ErrorEvent类型的错误事件时会调用指定给该属性的处理程序。
❑ port:专门用来跟共享线程通信的MessagePort。

4.SharedWorkerGlobalScope

在共享线程内部,全局作用域是SharedWorkerGlobalScope的实例。
SharedWorkerGlobal-Scope继承自WorkerGlobalScope,因此包括它所有的属性和方法。
与专用工作者线程一样,共享工作者线程也可以通过self关键字访问该全局上下文。

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

共享工作者线程的生命周期具有与专用工作者线程相同的阶段的特性。
不同之处在于:
专用工作者线程只跟一个页面绑定,共享工作者线程只要还有一个上下文连接就会持续存在。
关键在于,没有办法以编程方式终止共享线程。
前面已经交代过,SharedWorker对象上没有terminate()方法。在共享线程端口(稍后讨论)上调用close()时,只要还有一个端口连接到该线程就不会真的终止线程。
SharedWorker的“连接”与关联MessagePort或MessageChannel的状态无关。
只要建立了连接,浏览器会负责管理该连接。
建立的连接会在页面的生命周期内持续存在,只有当页面销毁且没有连接时,浏览器才会终止共享线程。

27.3.3 连接到共享工作者线程

每次调用SharedWorker()构造函数,无论是否创建了工作者线程,都会在共享线程内部触发connect事件。
发生connect事件时,SharedWorker()构造函数会隐式创建MessageChannel实例,并把MessagePort实例的所有权唯一地转移给该SharedWorker的实例。这个MessagePort实例会保存在connect事件对象的ports数组中。一个连接事件只能代表一个连接,因此可以假定ports数组的长度等于1。
关键在于,共享线程与父上下文的启动和关闭不是对称的。
每个新SharedWorker连接都会触发一个事件,但没有事件对应断开SharedWorker实例的连接(如页面关闭)。
在前面的例子中,随着与相同共享线程连接和断开连接的页面越来越多,connectedPorts集合中会受到死端口的污染,没有办法识别它们。
一个解决方案是:在beforeunload事件即将销毁页面时,明确发送卸载消息,让共享线程有机会清除死端口。

27.4 服务工作者线程

服务工作者线程(service worker)是一种类似浏览器中代理服务器的线程,可以拦截外出请求和缓存响应。
这可以让网页在没有网络连接的情况下正常使用,因为部分或全部页面可以从服务工作者线程缓存中提供服务。
服务工作者线程也可以使用Notifications API、Push API、Background Sync API和Channel Messaging API。
与共享工作者线程类似,来自一个域的多个页面共享一个服务工作者线程。不过,为了使用Push API等特性,服务工作者线程也可以在相关的标签页或浏览器关闭后继续等待到来的推送事件。
无论如何,对于大多数开发者而言,服务工作者线程在两个主要任务上最有用:
充当网络请求的缓存层,和启用推送通知。
在这个意义上,服务工作者线程就是用于 把网页变成像原生应用程序一样的工具。

27.4.1 服务工作者线程基础

1.ServiceWorkerContainer

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

2.创建服务工作者线程

与共享工作者线程类似,服务工作者线程同样是:在还不存在时创建新实例,在存在时连接到已有实例。ServiceWorkerContainer没有通过全局构造函数创建,而是暴露了register()方法,该方法以与Worker()或SharedWorker()构造函数相同的方式 传递脚本URL。
register()方法返回一个期约,该期约解决为ServiceWorkerRegistration对象,或在注册失败时拒绝。
注册服务工作者线程的一种非常常见的模式是:基于特性检测,并在页面的load事件中操作。
如果没有load事件这个门槛,服务工作者线程的注册就会与页面资源的加载重叠,进而拖慢初始页面渲染的过程。除非该服务工作者线程负责管理缓存(这样的话就需要尽早注册,比如使用本章稍后会讨论的clients.claim()),否则等待load事件是个明智的选择,这样同样可以发挥服务工作者线程的价值。

3.使用ServiceWorkerContainer对象

ServiceWorkerContainer接口是:浏览器对服务工作者线程生态的顶部封装。它为管理服务工作者线程状态,和生命周期提供了便利。
❑ oncontrollerchange:在ServiceWorkerContainer触发controllerchange事件时会调用指定的事件处理程序。
❑ 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()派发的消息。

4.使用ServiceWorkerRegistration对象

ServiceWorkerRegistration对象,表示注册成功的服务工作者线程。
该对象可以在register()返回的解决期约的处理程序中访问到。
通过它的一些属性可以确定关联服务工作者线程的生命周期状态。
调用navigator.serviceWorker.register()之后返回的期约,会将注册成功的Service-WorkerRegistration对象(注册对象)发送给处理函数。在同一页面使用同一URL多次调用该方法会返回相同的注册对象。
ServiceWorkerRegistration支持以下事件处理程序:
❑ onupdatefound:在服务工作者线程触发updatefound事件时会调用指定的事件处理程序。
ServiceWorkerRegistration支持以下通用属性:
scope, navigationPreload, pushManager,
ServiceWorkerRegistration还支持以下属性,可用于判断服务工作者线程处于生命周期的什么阶段:
installing, waiting, active
ServiceWorkerRegistration支持下列方法:
❑ getNotifications():返回期约,解决为Notification对象的数组。
❑ showNotifications():显示通知,可以配置title和options参数。
❑ update():直接从服务器重新请求服务脚本,如果新脚本不同,则重新初始化。
❑ unregister():取消服务工作者线程的注册。该方法会在服务工作者线程执行完再取消注册。

5.使用ServiceWorker对象

ServiceWorker对象可以通过两种方式获得:
通过ServiceWorkerContainer对象的controller属性,和通过ServiceWorkerRegistration的active属性。
该对象继承Worker原型,因此包括其所有属性和方法,但没有terminate()方法。

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

① 与其他工作者线程一样,服务工作者线程也受加载脚本对应源的常规限制(更多信息参见27.2.1节)。
② 由于服务工作者线程几乎可以任意修改和重定向网络请求,以及加载静态资源,服务工作者线程API只能在安全上下文(HTTPS)下使用。在非安全上下文(HTTP)中,navigator.serviceWorker是undefined。
为方便开发,浏览器豁免了通过localhost或127.0.0.1在本地加载的页面的安全上下文规则。
注:可以通过window.isSecureContext确定当前上下文是否安全。

7.ServiceWorkerGlobalScope

在服务工作者线程内部,全局上下文是ServiceWorkerGlobalScope的实例。ServiceWorker-GlobalScope继承自WorkerGlobalScope,因此拥有它的所有属性和方法。服务工作者线程可以通过self关键字访问该全局上下文。
ServiceWorkerGlobalScope通过以下属性和方法扩展了WorkerGlobalScope:
❑ caches:返回服务工作者线程的CacheStorage对象。
❑ clients:返回服务工作者线程的Clients接口,用于访问底层Client对象。
❑ registration:返回服务工作者线程的ServiceWorkerRegistration对象。
❑ skipWaiting():强制服务工作者线程进入活动状态;需要跟Clients.claim()一起使用。
❑ fetch():在服务工作者线程内发送常规网络请求;用于在服务工作者线程确定有必要发送实际网络请求(而不是返回缓存值)时。
虽然专用工作者线程和共享工作者线程只有一个message事件作为输入,但服务工作者线程则可以接收很多事件,包括页面操作、通知操作触发的事件或推送事件。
服务工作者线程的全局作用域可以监听以下事件,这里进行了分类:
● 服务工作者线程状态
❑ install:在服务工作者线程进入安装状态时触发(在客户端可以通过ServiceWorker-Registration.installing判断)。也可以在self.onintall属性上指定该事件的处理程序。
❑ activate:在服务工作者线程进入激活或已激活状态时触发(在客户端可以通过ServiceWorkerRegistration.active判断)。也可以在self.onactive属性上指定该事件的处理程序。
● Fetch API
❑ fetch:在服务工作者线程截获来自主页面的fetch()请求时触发。服务工作者线程的fetch事件处理程序可以访问FetchEvent,可以根据需要调整输出。也可以在self.onfetch属性上指定该事件的处理程序。
● Message API
❑ message:在服务工作者线程通过postMesssage()获取数据时触发。也可以在self.onmessage属性上指定该事件的处理程序。
● Notification API
❑ notificationclick:在系统告诉浏览器用户点击了ServiceWorkerRegistration.showNoti-fication()生成的通知时触发。也可以在self.onnotificationclick属性上指定该事件的处理程序。
❑ notificationclose:在系统告诉浏览器用户关闭或取消显示了ServiceWorkerRegistration. showNotification()生成的通知时触发。也可以在self.onnotificationclose属性上指定该事件的处理程序。
● Push API
❑ push:在服务工作者线程接收到推送消息时触发。也可以在self.onpush属性上指定该事件的处理程序。
❑ pushsubscriptionchange:在应用控制外的因素(非JavaScript显式操作)导致推送订阅状态变化时触发。也可以在self.onpushsubscriptionchange属性上指定该事件的处理程序。

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

服务工作者线程只能拦截其作用域内的客户端发送的请求。作用域是相对于获取服务脚本的路径定义的。如果没有在register()中指定,则作用域就是服务脚本的路径。
服务工作者线程的作用域实际上遵循了目录权限模型,即只能相对于服务脚本所在路径缩小作用域。

27.4.2 服务工作者线程缓存

在服务工作者线程之前,网页缺少缓存网络请求的稳健机制。浏览器一直使用HTTP缓存,但HTTP缓存并没有对JavaScript暴露编程接口,且其行为是受JavaScript运行时外部控制的。可以开发临时缓存机制,缓存响应字符串或blob,但这种策略比较麻烦且效率低。
服务工作者线程的一个主要能力是:可通过编程方式实现真正的网络请求缓存机制。
与HTTP缓存或CPU缓存不同,服务工作者线程缓存非常简单:
❑ 服务工作者线程缓存不自动缓存任何请求。所有缓存都必须明确指定。
❑ 服务工作者线程缓存没有到期失效的概念。除非明确删除,否则缓存内容一直有效。
❑ 服务工作者线程缓存必须手动更新和删除
❑ 缓存版本必须手动管理。每次服务工作者线程更新,新服务工作者线程负责提供新的缓存键以保存新缓存。
唯一的浏览器强制逐出策略基于服务工作者线程缓存占用的空间。服务工作者线程负责管理自己缓存占用的空间。缓存超过浏览器限制时,浏览器会基于最近最少使用(LRU, Least Recently Used)原则为新缓存腾出空间。
本质上,服务工作者线程缓存机制是一个双层字典。
其中顶级字典的条目映射到二级嵌套字典。顶级字典是CacheStorage对象,可以通过服务工作者线程全局作用域的caches属性访问。顶级字典中的每个值都是一个Cache对象,该对象也是个字典,是Request对象到Response对象的映射。与LocalStorage一样,Cache对象在CacheStorage字典中无限期存在,会超出浏览器会话的界限。此外,Cache条目只能以源为基础存取。

1.CacheStorage对象

CacheStorage对象是映射到Cache对象的字符串键/值存储。CacheStorage提供的API类似于异步Map。CacheStorage的接口通过全局对象的caches属性暴露出来。
CacheStorage中的每个缓存可以通过给caches.open()传入相应字符串键取得。非字符串键会转换为字符串。如果缓存不存在,就会创建。
与Map类似,CacheStorage也有has()、delete()和keys()方法。这些方法与Map上对应方法类似,但都基于期约。
CacheStorage接口还有一个match()方法,可根据Request对象搜索CacheStorage中的所有Cache对象。搜索顺序是CacheStorage.keys()的顺序,返回匹配的第一个响应。

2.Cache对象

CacheStorage通过字符串映射到Cache对象。Cache对象跟CacheStorage一样,类似于异步的Map。Cache键可以是URL字符串,也可以是Request对象。这些键会映射到Response对象。
为填充Cache,可能使用以下三个方法:
❑ put(request, response)
❑ add(request)
❑ addAll(requests)
与Map类似,Cache也有delete()和keys()方法。这些方法与Map上对应方法类似,但都基于期约。
要检索Cache,可以使用下面的两个方法。
❑ matchAll(request, options):返回期约,期约解决为匹配缓存中Response对象的数组。
❑ match(request, options):返回期约,期约解决为匹配缓存中的Response对象;如果没命中缓存则返回undefined。

3.最大存储空间

使用StorageEstimate API可以近似地获悉有多少空间可用(以字节为单位),以及当前使用了多少空间。此方法只在安全上下文中可用。
这些并不是确切的数值,考虑到压缩、去重和混淆等安全原因,该数字并不精确。

27.4.3 服务工作者线程客户端

服务工作者线程会使用Client对象跟踪关联的窗口、工作线程或服务工作者线程。
服务工作者线程可通过Clients接口访问这些Client对象。该接口暴露在全局上下文的self.clients属性上。
Client对象支持以下属性和方法:
id, type, url, postMessage()
Clients接口支持通过get()或matchAll()访问Client对象。这两个方法都通过期约返回结果。matchAll()也可以接收options对象,该对象支持以下属性:
includeUncontrolled, type
Clients接口也支持以下方法:❑ openWindow(url) ❑ claim()

27.4.4 服务工作者线程与一致性

理解服务工作者线程最终用途十分重要:
让网页能够模拟原生应用程序。要像原生应用程序一样,服务工作者线程必须支持版本控制(versioning)。
从全局角度说,服务工作者线程的版本控制可确保任何时候两个网页的操作都有一致性
该一致性可以表现为如下两种形式:
❑ 代码一致性。网页不是像原生应用程序那样基于一个二进制文件创建,而是由很多HTML、CSS、JavaScript、图片、JSON,以及页面可能加载的任何类型的文件创建。网页经常会递增更新,即版本升级,以增加或修改行为。如果网页总共加载了100个文件,而加载的资源同时来自第1版和第2版,那么就会导致完全无法预测,而且很可能出错。服务工作者线程为此提供了一种强制机制,确保来自同源的所有并存页面始终会使用来自相同版本的资源。
❑ 数据一致性。网页并非与外界隔绝的应用程序。它们会通过各种浏览器API如LocalStorage或IndexedDB在本地读取并写入数据;也会向远程API发送请求并获取数据。这些获取和写入数据的格式在不同版本中可能也会变化。如果一个页面以第1版中的格式写入了数据,第二个页面以第2版中的格式读取该数据就会导致无法预测的结果甚至出错。服务工作者线程的资源一致性机制可以保证网页输入/输出行为对同源的所有并存网页都相同。
为确保一致性,服务工作者线程的生命周期不遗余力地避免出现有损一致性的现象。
比如下面这些可能:
❑ 服务工作者线程提早失败。
❑ 服务工作者线程激进更新。
❑ 未激活服务工作者线程消极活动。
❑ 活动的服务工作者线程粘连。

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

Service Worker规范定义了6种服务工作者线程可能存在的状态:
已解析(parsed)、安装中(installing)、已安装(installed)、激活中(activating)、已激活(activated)已失效(redundant)
完整的服务工作者线程生命周期会以该顺序进入相应状态,尽管有可能不会进入每个状态。安装或激活服务工作者线程时遇到错误会跳到已失效状态。
上述状态的每次变化都会在ServiceWorker对象上触发statechange事件

1.已解析状态

调用navigator.serviceWorker.register()会启动创建服务工作者线程实例的过程。刚创建的服务工作者线程实例会进入已解析状态。该状态没有事件,也没有与之相关的ServiceWorker.state值。

2.安装中状态

安装中状态是执行所有服务工作者线程设置任务的状态。这些任务包括在服务工作者线程控制页面前必须完成的操作。
在客户端,这个阶段可以通过检查ServiceWorkerRegistration.installing是否被设置为ServiceWorker实例。
安装中状态频繁用于填充服务工作者线程的缓存。服务工作者线程在成功缓存指定资源之前可以一直处于该状态。如果任何资源缓存失败,服务工作者线程都会安装失败并跳至已失效状态。
如果没有错误发生或者没有拒绝,服务工作者线程就会前进到已安装状态。

3.已安装状态

已安装状态也称为等待中(waiting)状态,意思是服务工作者线程此时没有别的事件要做,只是准备在得到许可的时候去控制客户端。如果没有活动的服务工作者线程,则新安装的服务工作者线程会跳到这个状态,并直接进入激活中状态,因为没有必要再等了。
在客户端,这个阶段可以通过检查ServiceWorkerRegistration.waiting是否被设置为一个ServiceWorker实例来确定。
如果已有了一个活动的服务工作者线程,则已安装状态是触发逻辑的好时机,这样会把这个新服务工作者线程推进到激活中状态。可以通过self.skipWaiting()强制推进服务工作者线程的状态,也可以通过提示用户重新加载应用程序,从而使浏览器可以按部就班地推进。

4.激活中状态

激活中状态表示服务工作者线程已经被浏览器选中即将变成可以控制页面的服务工作者线程。
如果浏览器中没有活动服务工作者线程,这个新服务工作者线程会自动到达激活中状态。如果有一个活动服务工作者线程,则这个作为替代的服务工作者线程可以通过如下方式进入激活中状态。
❑ 原有服务工作者线程控制的客户端数量变为0。这通常意味着所有受控的浏览器标签页都被关闭。在下一个导航事件时,新服务工作者线程会到达激活中状态。
❑ 已安装的服务工作者线程调用self.skipWaiting()。这样可以立即生效,而不必等待一次导航事件。在激活中状态下,不能像已激活状态中那样执行发送请求或推送事件的操作。
在客户端,这个阶段大致可以通过检查ServiceWorkerRegistration.active是否被设置为一个ServiceWorker实例来确定。

5.已激活状态

已激活状态表示服务工作者线程正在控制一个或多个客户端。在这个状态,服务工作者线程会捕获其作用域中的fetch()事件、通知和推送事件。
在客户端,这个阶段大致可以通过检查ServiceWorkerRegistration.active是否被设置为一个ServiceWorker实例来确定。
注:ServiceWorkerRegistration.active属性表示服务工作者线程可能在激活中状态,也可能在已激活状态。
更可靠的确定服务工作者线程处于已激活状态一种方式是检查ServiceWorkerRegistration的controller属性。该属性会返回激活的ServiceWorker实例,即控制页面的实例。

6.已失效状态

已失效状态表示服务工作者线程已被宣布死亡。不会再有事件发送给它,浏览器随时可能销毁它并回收它的资源。

7.更新服务工作者线程

因为版本控制的概念根植于服务工作者线程的整个生命周期,所以服务工作者线程会随着版本变化。
为此,服务工作者线程提供了稳健同时也复杂的流程,以安装替换过时的服务工作者线程。
这个更新流程的初始阶段是更新检查,也就是浏览器重新请求服务脚本。
以下事件可以触发更新检查:
❑ 以创建当前活动服务工作者线程时不一样的URL调用navigator.serviceWorker.register()。
❑ 浏览器导航到服务工作者线程作用域中的一个页面。
❑ 发生了fetch()或push()等功能性事件,且至少24小时内没有发生更新检查。
新获取的服务脚本会与当前服务工作者线程的脚本比较差异。如果不相同,浏览器就会用新脚本初始化一个新的服务工作者线程。更新的服务工作者线程进入自己的生命周期,直至抵达已安装状态。到达已安装状态后,更新服务工作者线程会等待浏览器决定让它安全地获得页面的控制权(或用户强制它获得页面控制权)。
关键在于,刷新页面不会让更新服务工作者线程进入激活状态并取代已有的服务工作者线程。
比如,有个打开的页面,其中有一个服务工作者线程正在控制它,而一个更新服务工作者线程正在已安装状态中等待。客户端在页面刷新期间会发生重叠,即旧页面还没有卸载,新页面已加载了。因此,现有的服务工作者线程永远不会让出控制权,毕竟至少还有一个客户端在它的控制之下。为此,取代现有服务工作者线程唯一的方式就是关闭所有受控页面。

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

虽然专用工作者线程和共享工作者线程是有状态的,但服务工作者线程是无状态的。
更具体地说,服务工作者线程遵循控制反转(IoC, Inversion ofControl)模式并且是事件驱动的。
这样就意味着:
服务工作者线程不应该依赖工作者线程的全局状态。服务工作者线程中的绝大多数代码应该在事件处理程序中定义。当然,服务工作者线程的版本作为全局常量是个显而易见的例外。
服务脚本执行的次数变化很大,高度依赖浏览器状态,因此服务脚本的行为应该是幂等的。
理解服务工作者线程的生命周期与它所控制的客户端的生命周期无关非常重要。
大多数浏览器将服务工作者线程实现为独立的进程,而该进程由浏览器单独控制。
如果浏览器检测到某个服务工作者线程空闲了,就可以终止它并在需要时再重新启动。
这意味着:可以依赖服务工作者线程在激活后处理事件,但不能依赖它们的持久化全局状态。

27.4.7 通过updateViaCache管理服务文件缓存

为了让客户端能控制自己的更新行为,可以通过updateViaCache属性设置客户端对待服务脚本的方式。该属性可以在注册服务工作者线程时定义,可以是如下三个字符串值:imports, all, none

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

某些情况下,有必要尽可能快地让服务工作者线程进入已激活状态,即使可能会造成资源版本控制不一致。
该操作通常适合在安装事件中缓存资源,此时要强制服务工作者线程进入活动状态,然后再强制活动服务工作者线程去控制关联的客户端。

27.4.9 服务工作者线程消息

与专用工作者线程和共享工作者线程一样,服务工作者线程也能与客户端通过postMessage()交换消息。
实现通信的最简单方式是向活动工作者线程发送一条消息,然后使用事件对象发送回应。
发送给服务工作者线程的消息可以在全局作用域处理,而发送回客户端的消息则可以在ServiceWorker-Context对象上处理。
因为客户端和服务工作者线程可以相互之间发送消息,所以通过MessageChannel或Broadcast-Channel实现通信也是可能的。

27.4.10 拦截fetch事件

服务工作者线程最重要的一个特性就是拦截网络请求。
服务工作者线程作用域中的网络请求会注册为fetch事件。这种拦截能力不限于fetch()方法发送的请求,也能拦截对JavaScript、CSS、图片和HTML(包括对主HTML文档本身)等资源发送的请求。
FetchEvent继承自ExtendableEvent。让服务工作者线程能够决定如何处理fetch事件的方法是event.respondWith()。该方法接收期约,该期约会解决为一个Response对象。当然,该Response对象实际上来自哪里完全由服务工作者线程决定。可以来自网络,来自缓存,或者动态创建。
下面几节将介绍几种网络/缓存策略,可以在服务工作者线程中使用:

1.从网络返回

这个策略就是简单地转发fetch事件。那些绝对需要发送到服务器的请求例如POST请求就适合该策略。

  1. self.onfetch = (fetchEvent) => {
  2. frameEvent.respondWith(fetch(frameEvent.request));
  3. };

2.从缓存返回

这个策略其实就是缓存检查。对于任何肯定有缓存的资源(如在安装阶段缓存的资源),可以采用该策略

  1. self.onfetch = (fetchEvent) => {
  2. frameEvent.respondWith(caches.match(frameEvent.request));
  3. };

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

这个策略把从网络获取最新的数据作为首选,但如果缓存中有值也会返回缓存的值。
如果应用程序需要尽可能展示最新数据,但在离线的情况下仍要展示一些信息,就可以采用该策略

  1. self.onfetch = (fetchEvent) => {
  2. frameEvent.respondWith(
  3. fetch(frameEvent.request)
  4. .catch(() => caches.match(frameEvent.request))
  5. );
  6. };

4.从缓存返回,网络作后备

这个策略优先考虑响应速度,但仍会在没有缓存的情况下发送网络请求。
这是大多数渐进式Web应用程序(PWA, Progressive Web Application)采取的首选策略

  1. self.onfetch = (fetchEvent) => {
  2. frameEvent.respondWith(
  3. caches.match(frameEvent.request)
  4. .then((response) => response || fetch(frameEvent.request))
  5. );
  6. };

5.通用后备

应用程序需要考虑缓存和网络都不可用的情况。
服务工作者线程可以在安装时缓存后备资源,然后在缓存和网络都失败时返回它们

  1. self.onfetch = (fetchEvent) => {
  2. frameEvent.respondWith(
  3. // 开始执行“从缓存返回,以网络为后备”策略
  4. caches.match(frameEvent.request)
  5. .then((response) => response || fetch(frameEvent.request))
  6. .catch(() => caches.match('/fallback.html'))
  7. );
  8. };

这里的catch()子句可以扩展为支持不同类型的后备,例如点位图、哑数据,等等。

27.4.11 推送通知

网页必须能够接收服务器的推送事件,然后在设备上显示通知(即使应用程序没有运行)。服务工作者线程可实现该行为。
为了在PWA应用程序中支持推送通知,必须支持以下4种行为:
❑ 服务工作者线程必须能够显示通知。
❑ 服务工作者线程必须能够处理与这些通知的交互。
❑ 服务工作者线程必须能够订阅服务器发送的推送通知。
❑ 服务工作者线程必须能够处理推送消息,即使应用程序没在前台运行或者根本没打开。

1.显示通知

服务工作者线程可以通过它们的注册对象使用Notification API。
这样做有很好的理由:与服务工作者线程关联的通知,也会触发服务工作者线程内部的交互事件。
显示通知要求向用户明确地请求授权。授权完成后,可以通过ServiceWorkerRegistration. showNotification()显示通知。

2.处理通知事件

通过ServiceWorkerRegistration对象创建的通知会向服务工作者线程发送notificationclick和notificationclose事件。

3.订阅推送事件

对于发送给服务工作者线程的推送消息,必须通过服务工作者线程的PushManager来订阅。
这样服务工作者线程就可以在push事件处理程序中处理推送消息。

4.处理推送事件

订阅之后,服务工作者线程会在每次服务器推送消息时收到push事件。
push事件继承了ExtendableEvent。可以把showNotification()返回的期约传给waitUntil(),这样就会让服务工作者线程一直活动到通知的期约解决。