二十七、工作者线程之服务工作者

服务工作者线程(service worker)是一种类似浏览器中代理服务器的线程,可以拦截外出请求和缓存响应。
这可以让网页在没有网络连接的情况下正常使用,因为部分或全部页面可以从服务工作者线程缓存中提供服务。
服务工作者线程也可以使用 Notifications API、Push API、Background Sync API 和Channel Messaging API。
与共享工作者线程类似,来自一个域的多个页面共享一个服务工作者线程。
不过,为了使用 Push API等特性,服务工作者线程也可以在相关的标签页或浏览器关闭后继续等待到来的推送事件。

对于大多数开发者而言,服务工作者线程在两个主要任务上最有用:充当网络请求的缓存层启用推送通知

1服务工作者线程基础

1.1ServiceWorkerContainer

服务工作者线程没有全局构造函数,而是通过ServiceWorkerContainer管理,其实例保存在navigator.serviceWorker属性中。

console.log(navigator.serviceWorker);// ServiceWorker{...}

1.2 创建服务工作者线程

服务工作者线程同样是在还不存在时创建新实例,在存在时连接到已有实
。ServiceWorkerContainer 没有通过全局构造函数创建,而是暴露了 register()方法,该方法以与 Worker()或 SharedWorker()构造函数相同的方式传递脚本 URLregister()返回一个期约,解决为ServiceWorkerGlobalScope对象,在注册失败时拒绝。

navigator.serviceWorker.register('./emptyServiceWorker.js').then(console.log, console.error);

注册服务工作者线程的一种非常常见的模式是基于特性检测,并在页面的 load 事件中操作。

if ('serviceWorker' in navigator) {
	 window.addEventListener('load', () => {
	 	navigator.serviceWorker.register('./serviceWorker.js');
	 });
}

1.3 使用ServiceWorkerContainer对象

支持的事件处理程序

事件处理程序 说明
oncontrollerchange 在获得新激活的ServiceWorkerRegistration时触发
onerror 服务工作者线程内部触发ErrorEvent错误类型时触发,或者服务工作者内部抛出错误时触发
onmessage 件在服务脚本向父上下文发送消息时触发

支持的属性和方法

属性或者方法 说明
ready 返回期约,解决为激活的ServiceWorkerRegistration对象,该期约不会拒绝
controller 返回与当前页面关联的激活的ServiceWorker对象,没有时返回null
register() 接受url和配置options,创建或者更新ServiceWorkerRegistration
getRegisteration() 返回期约,解决为与提供作用域匹配的ServiceWorkerRegistration对象,没有就返回undefined
getRegistrations() 返回期约,解决为与ServiceWorkerContainer关联的ServiceWorkerRegistration对象数组,没有时返回空数组
startMessage() 开始通过Client.postMessage()派发消息

1.4使用ServiceWorkerRegistration对象

使用navigator.serviceWorker.register()返回的期约解决为ServiceWorkerRegistration对象。多次使用同一个url进行注册,会返回同一个ServiceWorkerRegistration对象。
支持的事件处理程序

事件 说明
onupdatefound 服务工作者线程触发updatefound事件时发生,此事件在服务工作者线程开始安装新版本时触发,表现为ServiceWorkerRegistration.installing收到一个新的服务工作者线程

支持的属性和方法

属性或者方法 说明
scope 返回服务工作者完成的url路径
navigationPreload 返回与注册对象关联的NavigationPreloadManager对象
pushManage 返回与注册对象关联的PushManager实例
installing 若存在,则返回状态为installing的服务工作者线程,否则为null
waiting 若存在,则返回状态为waiting的服务工作者线程,否则为null
active 若存在,则返回状态为activating或者active的服务工作者线程
getNotifications() 返回期约,解决为Notification对象的数组
showNotifications() 显示通知,可以配置title和options参数
update() 直接从服务器重新请求服务脚本,如果新脚本不同,则重新初始化
unregister() 取消服务工作者线程的注册,该方法会在服务工作者线程执行完在取消注册

1.5使用ServiceWorker对象

获取该对象的方式:

  • navigator.serviceWorker.controller,通过ServiceWorkerContainer对象的controller属性
  • 通过ServiceWorkerRegistration对象active属性

支持的事件处理程序

事件 说明
onstatechange 状态发生变化的时候触发

支持的属性和方法

属性或者方法 说明
scriptUrl 解析后注册服务工作者线程的URL
state 服务工作者线程的状态, installing/installed/activating/activated/redundant

1.6服务工作者的安全限制

受到加载脚本对应源的常规限制;
只能在安全上下文使用(HTTPS);
可以通过window.isSecureContext判断是否是安全上下文

1.7 ServiceWorkerGlobalScope

继承自WorkerGlobalScope,扩展了以下属性和方法

属性或者方法 说明
caches 返回服务工作者线程的CacheStorage对象
clients 返回服务工作者线程的Clients接口,用于访问底层Client对象
registration 返回服务工作者线程的ServiceWorkerRegistration对象
skipWaiting() 强制服务工作者线程进入活动状态,需要跟Clients.claim()一起使用
fetch() 在服务工作者线程内发送常规网络请求,用于在服务工作者线程确实有必要发送实际网络请求(而不是返回缓存值)

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

  • 服务工作者线程状态
    • install, 在服务工作者线程进入安装状态时触发, 每个服务工作者线程接受到的第一个事件,每个服务工作者只会调用一次
    • activate, 在服务工作者进入激活或者已激活状态时触发
  • Fetch API
    • fetch, 在服务工作者线程截获来自主页面的fetch()请求时触发
  • Message API
    • message, 在服务工作者通过postMessage()获取数据时触发
  • Notification API
    • notificationclick, 在系统告诉浏览器用户点击了ServiceWorkerRegistration.showNotification()生成的通知时触发
    • notificationclose, 在系统告诉浏览器用户关闭或者取消了显示ServiceWorkerRegistration.showNotification()生成的通知时触发
  • Push API
    • push, 在服务工作者线程接收到推送消息时触发
    • pushsubscriptionchange, 在应用控制以外的因素导致推送状态发生变化时触发

1.8 服务工作者线程作用域的限制

服务工作者线程只能拦截作用域内的客户端发送的请求
作用域是相对于获取服务脚本的路径定义的,若是没有明确指定,则作用域为服务脚本的路径,遵循目录权限模型

扩展服务工作者作用域的方式有两种

  • 通过包含想要的作用域的路径提供服务脚本
  • 给服务脚本的响应添加Service-Worker-Allowed头部,将其值设置为想要的作用域。

2. 服务工作者线程缓存

在服务工作者线程之前,网页缺少缓存网络请求的稳健机制。浏览器一直使用 HTTP 缓存,但 HTTP缓存并没有对 JavaScript 暴露编程接口,且其行为是受 JavaScript 运行时外部控制的

  • 服务工作者线程缓存不自动缓存任何请求
  • 服务工作者线程缓存没有到期失效的概念
  • 服务工作者线程缓存必须手动更新和删除
  • 缓存版本必须手动管理
  • 唯一的浏览器强制逐出策略基于服务工作者线程缓存占用的空间
    本质上,服务工作者线程缓存机制是一个双层字典,其中顶级字典的条目映射到二级嵌套字典。顶级字典是 CacheStorage 对象,可以通过服务工作者线程全局作用域的 caches 属性访问。顶级字典中的每个值都是一个 Cache 对象,该对象也是个字典,是 Request 对象到 Response 对象的映射

2.1 CacheStorage对象

类似于异步Map, 字符串到Cache对象的映射,通过全局对象的caches属性暴露
has()/delete()/keys()/等方法与Map类似,但是基于期约实现,获取缓存的方法是open(strKey)

caches.open('v1').then(console.log);// Cache{...}
caches.has('v1').then(console.log);// true
caches.delete('v1').then(() => caches.has('v1')).then(console.log); // false
caches.keys().then(console.log) // ['v1',...]

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

// 创建一个请求键和两个响应值
const request = new Request('');
const response1 = new Response('v1');
const response2 = new Response('v2');
// 用同一个键创建两个缓存对象,最终会先找到 v1
// 因为它排在 caches.keys()输出的前面
caches.open('v1')
.then((v1cache) => v1cache.put(request, response1))
.then(() => caches.open('v2'))
.then((v2cache) => v2cache.put(request, response2))
.then(() => caches.match(request))
.then((response) => response.text())
.then(console.log); // v1

2.2 Cache对象

Cache 键可以是 URL 字符串,也可以是 Request 对象。这些键会映射到Response 对象;
服务工作者线程缓存只考虑缓存 HTTP 的 GET 请求

  • 填充Cache对象的方法

    • put(request, response),在键(Request对象或者URL字符串)和值同时存在的情况下用于添加缓存项
    • add(request), 只有Request对象或者URL时,使用此方法发送fetch()请求,并缓存响应。
    • addAll(requests),对每一项分别调用add()

  • 检索Cache的方法

    • matchAll(request, options),返回期约,解决为匹配缓存中的Response对象数组,可以对结构类似的缓存进行批量操作,如删除缓存在/images目录的值
    • match(request, options), 相当于matchAll(request, options)[0]

  • 缓存是否命中取决于 URL 字符串和/或 Request 对象 URL 是否匹配

  • Cache 对象使用 Request 和 Response 对象的 clone()方法创建副本
    Cache.match()/Cache.matchAll()/CacheStorage.match()支持可选的配置项options参数

配置项 说明
cacheName 只有CacheStorage.matchAll()支持,设置字符串时,只会匹配Cache键为指定字符串的缓存值
ignoreSearch boolean,匹配url时忽略查询字符串
ignoreMethod boolean, 设置为true时,匹配url时忽略查询请求的HTTP方法
ignoreVary 匹配时是否忽略HTTP头部的Vary头部
const request1 = new Request('https://www.foo.com');
const response1 = new Response('fooResponse', { headers: {'Vary': 'Accept' }});
const acceptRequest1 = new Request('https://www.foo.com',{ headers: { 'Accept': 'text/json' } });
caches.open('v1')
	.then((cache) => {
 		cache.put(request1, response1)
 			.then(() => cache.match(acceptRequest1))
 			.then(console.log) // undefined
 			.then(() => cache.match(acceptRequest1, { ignoreVary: true }))
 			.then(console.log); // Response {}
}); 

2.3 最大存储空间

使用 StorageEstimate API 可以近似地获悉有多少空间可用(以字节为单位),以及当前使用了多少空间。此方法只在安全上下文中可用

navigator.storage.estimate().then(console.log); 

3. 服务工作者线程客户端

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

属性或者方法 说明
id 返回客户端的全局唯一标识符
type 返回表示客户端类型的字符串window/worker/sharedworker/
url 返回客户端的url
postMessage() 用于向单个客户端发送消息

Clients接口支持的方法

方法 说明
get() 通过期约返回Client对象,接受id为参数
matchAll() 通过期约返回Client对象, 接受options参数配置项
openWindow(url) 在新的窗口打开指定url,给当前服务工作者添加一个新的Client
claim() 强制设置当前服务工作者线程以控制作用域中的所有客户端

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

理解服务工作者线程最终用途十分重要:让网页能够模拟原生应用程序。要像原生应用程序一样,服务工作者线程必须支持版本控制(versioning)。

  • 代码一致性
  • 数据一致性

避免有损一致性的现象

  • 服务者工作线程提早失败
  • 服务工作者线程激进更新
  • 未激活的服务工作者线程消极活动
  • 活动的服务工作者线程粘连

5. 服务工作者线程的生命周期

Service Worker 规范定义了 6 种服务工作者线程可能存在的状态:已解析(parsed)、安装中(installing)、已安装(installed)、激活中(activating)、已激活(activated)和已失效(redundant)。
上述状态的每次变化都会在 ServiceWorker 对象上触发 statechange 事件.

6.控制反转和服务工作者线程持久化

  • 虽然专用工作者线程和共享工作者线程是有状态的,但服务工作者线程是无状态的。更具体地说,服务工作者线程遵循控制反转(IoC,Inversion of Control)模式并且是事件驱动的。
  • 工作者线程的生命周期与它所控制的客户端的生命周期无关

7. 通过updateViaCache管理服务文件缓存

为了尽可能传播更新后的服务脚本,常见的解决方案是在响应服务脚本时设置 Cache-Control:max-age=0 头部。这样浏览器就能始终取得最新的脚本文件。但是这个方案只能由服务端控制客户端
为了让客户端能控制自己的更新行为,可以通过 updateViaCache 属性设置客户端对待服务脚本的方式。该属性可以在注册服务工作者线程时定义,可以是如下三个字符串值。

  • imports, 默认值,永远不会被缓存, importScripts()的脚本按照各自的cache-control决定
  • all,无特殊待遇,按照cache-control头部决定是否缓存
  • none, 顶级服务脚本和importScripts()的脚本都不会被缓存
navigator.serviceWorker.register('/serviceWorker.js', {
 updateViaCache: 'none'
}); 

8. 强制服务工作者线程操作

update()

9.服务工作者线程消息

postMessage()
客户端先发送消息,服务端接收到的参数中会有一个source指向客户端

/************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.reigster('./serviceWorker.js')
.then((registration) => {
	if (registration.active){
		registration.active.postMessage('foo');
	}
}
// main.js中也可以使用controller属性
navigator.serviceWorker.reigster('./serviceWorker.js')
.then(() => {
	if (navigator.serviceWorker.controller){
		navigator.serviceWorker.controller.postMessage('foo');
	}
}

若是要服务端先发送消息,可以使用clients.matchAll()获取客户端

/**********ServiceWorker.js*****/
self.onmessage = ({data}) => {
 console.log('service worker heard:', data);
};
self.onactivate = () => {
 self.clients.matchAll({includeUncontrolled: true})
 .then((clientMatches) => clientMatches[0].postMessage('foo'));
};
/***********main.js**************/
navigator.serviceWorker.onmessage = ({data, source}) => {
 console.log('client heard:', data);
 source.postMessage('bar');
};
navigator.serviceWorker.register('./serviceWorker.js') 

10.拦截fetch()事件

这种拦截能力不限于 fetch()方法发送的请求,也能拦截对 JavaScript、CSS、图片和HTML(包括对主 HTML 文档本身)等资源发送的请求。
FetchEvent 继承自 ExtendableEvent。让服务工作者线程能够决定如何处理 fetch 事件的方法是 event.respondWith()

10.1 从网络返回

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

10.2 从缓存返回

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

10.3 从网络返回,缓存作为后备

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

10.4 从缓存返回,网络作为后备

self.onfetch = (fetchEvent) => {
	 fetchEvent.respondWith(
		 caches.match(fetchEvent.request)
		 .then((response) => response || fetch(fetchEvent.request))
	 );
}; 

10.5通用后备

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

11.推送通知

为了在 PWA 应用程序中支持推送通知,必须支持以下 4 种行为。

  • 服务工作者线程必须能够显示通知。
  • 服务工作者线程必须能够处理与这些通知的交互。
  • 服务工作者线程必须能够订阅服务器发送的推送通知。
  • 服务工作者线程必须能够处理推送消息,即使应用程序没在前台运行或者根本没打开。

11.1 显示通知

navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
	 Notification.requestPermission()
	 	.then((status) => {
	 		if (status === 'granted') {
	 		registration.showNotification('foo');
	 	}
	 });
}); 
// 服务工作者内部也可
self.onactivate = () => self.registration.showNotification('bar');

11.2 处理通知事件

self.onnotificationclick = ({notification}) => {
 	console.log('notification click', notification);
};
self.onnotificationclose = ({notification}) => {
 	console.log('notification close', notification);
};

11.3 订阅推送事件

navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
	 registration.pushManager.subscribe({
		 applicationServerKey: key, // 来自服务器的公钥
		 userVisibleOnly: true
	 });
}); 

// 服务工作者内部
self.onactivate = () => {
	 self.registration.pushManager.subscribe({
		 applicationServerKey: key, // 来自服务器的公钥
		 userVisibleOnly: true
	 });
}; 

11.4 处理推送事件

/*************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');
}; 

你可能感兴趣的:(javascript,开发语言,服务工作者线程)