服务工作者线程(service worker)是一种类似浏览器中代理服务器的线程,可以拦截外出请求和缓存响应。
这可以让网页在没有网络连接的情况下正常使用,因为部分或全部页面可以从服务工作者线程缓存中提供服务。
服务工作者线程也可以使用 Notifications API、Push API、Background Sync API 和Channel Messaging API。
与共享工作者线程类似,来自一个域的多个页面共享一个服务工作者线程。
不过,为了使用 Push API等特性,服务工作者线程也可以在相关的标签页或浏览器关闭后继续等待到来的推送事件。
对于大多数开发者而言,服务工作者线程在两个主要任务上最有用:充当网络请求的缓存层和启用推送通知。
服务工作者线程没有全局构造函数,而是通过ServiceWorkerContainer
管理,其实例保存在navigator.serviceWorker
属性中。
console.log(navigator.serviceWorker);// ServiceWorker{...}
服务工作者线程同样是在还不存在时创建新实例,在存在时连接到已有实
例。ServiceWorkerContainer 没有通过全局构造函数创建,而是暴露了 register()
方法,该方法以与 Worker()或 SharedWorker()构造函数相同的方式传递脚本 URL
,register()
返回一个期约,解决为ServiceWorkerGlobalScope对象
,在注册失败时拒绝。
navigator.serviceWorker.register('./emptyServiceWorker.js').then(console.log, console.error);
注册服务工作者线程的一种非常常见的模式是基于特性检测,并在页面的 load 事件
中操作。
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./serviceWorker.js');
});
}
支持的事件处理程序
事件处理程序 | 说明 |
---|---|
oncontrollerchange | 在获得新激活的ServiceWorkerRegistration 时触发 |
onerror | 服务工作者线程内部触发ErrorEvent 错误类型时触发,或者服务工作者内部抛出错误时触发 |
onmessage | 件在服务脚本向父上下文发送消息时触发 |
支持的属性和方法
属性或者方法 | 说明 |
---|---|
ready | 返回期约,解决为激活的ServiceWorkerRegistration对象,该期约不会拒绝 |
controller | 返回与当前页面关联的激活的ServiceWorker对象,没有时返回null |
register() | 接受url和配置options,创建或者更新ServiceWorkerRegistration |
getRegisteration() | 返回期约,解决为与提供作用域匹配的ServiceWorkerRegistration对象,没有就返回undefined |
getRegistrations() | 返回期约,解决为与ServiceWorkerContainer关联的ServiceWorkerRegistration对象数组,没有时返回空数组 |
startMessage() | 开始通过Client.postMessage()派发消息 |
使用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() | 取消服务工作者线程的注册,该方法会在服务工作者线程执行完在取消注册 |
获取该对象的方式:
navigator.serviceWorker.controller
,通过ServiceWorkerContainer对象的controller属性ServiceWorkerRegistration对象
的active
属性支持的事件处理程序
事件 | 说明 |
---|---|
onstatechange | 状态发生变化的时候触发 |
支持的属性和方法
属性或者方法 | 说明 |
---|---|
scriptUrl | 解析后注册服务工作者线程的URL |
state | 服务工作者线程的状态, installing /installed /activating /activated /redundant |
受到加载脚本对应源的常规限制;
只能在安全上下文使用(HTTPS);
可以通过window.isSecureContext
判断是否是安全上下文
继承自WorkerGlobalScope,扩展了以下属性和方法
属性或者方法 | 说明 |
---|---|
caches | 返回服务工作者线程的CacheStorage对象 |
clients | 返回服务工作者线程的Clients接口,用于访问底层Client对象 |
registration | 返回服务工作者线程的ServiceWorkerRegistration对象 |
skipWaiting() | 强制服务工作者线程进入活动状态,需要跟Clients.claim()一起使用 |
fetch() | 在服务工作者线程内发送常规网络请求,用于在服务工作者线程确实有必要发送实际网络请求(而不是返回缓存值) |
install
, 在服务工作者线程进入安装状态时触发, 每个服务工作者线程接受到的第一个事件,每个服务工作者只会调用一次activate
, 在服务工作者进入激活或者已激活状态时触发fetch
, 在服务工作者线程截获来自主页面的fetch()请求时触发message
, 在服务工作者通过postMessage()获取数据时触发notificationclick
, 在系统告诉浏览器用户点击了ServiceWorkerRegistration.showNotification()
生成的通知时触发notificationclose
, 在系统告诉浏览器用户关闭或者取消了显示ServiceWorkerRegistration.showNotification()
生成的通知时触发push
, 在服务工作者线程接收到推送消息时触发pushsubscriptionchange
, 在应用控制以外的因素导致推送状态发生变化时触发服务工作者线程只能拦截作用域内的客户端发送的请求
作用域是相对于获取服务脚本的路径定义的,若是没有明确指定,则作用域为服务脚本的路径,遵循目录权限模型
扩展服务工作者作用域的方式有两种
Service-Worker-Allowed头部
,将其值设置为想要的作用域。在服务工作者线程之前,网页缺少缓存网络请求的稳健机制。浏览器一直使用 HTTP 缓存,但 HTTP缓存并没有对 JavaScript 暴露编程接口,且其行为是受 JavaScript 运行时外部控制的。
类似于异步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
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 {}
});
使用 StorageEstimate API
可以近似地获悉有多少空间可用(以字节为单位),以及当前使用了多少空间。此方法只在安全上下文中可用
navigator.storage.estimate().then(console.log);
服务工作者线程会使用 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() | 强制设置当前服务工作者线程以控制作用域中的所有客户端 |
理解服务工作者线程最终用途十分重要:让网页能够模拟原生应用程序。要像原生应用程序一样,服务工作者线程必须支持版本控制
(versioning)。
避免有损一致性的现象
Service Worker 规范定义了 6 种服务工作者线程可能存在的状态:已解析
(parsed)、安装中
(installing)、已安装
(installed)、激活中
(activating)、已激活
(activated)和已失效
(redundant)。
上述状态的每次变化都会在 ServiceWorker 对象
上触发 statechange 事件
.
控制反转
(IoC,Inversion of Control)模式并且是事件驱动
的。为了尽可能传播更新后的服务脚本,常见的解决方案是在响应服务脚本时设置 Cache-Control:max-age=0
头部。这样浏览器就能始终取得最新的脚本文件。但是这个方案只能由服务端控制客户端
为了让客户端能控制自己的更新行为,可以通过 updateViaCache 属性
设置客户端对待服务脚本的方式。该属性可以在注册服务工作者线程时定义,可以是如下三个字符串值。
imports
, 默认值,永远不会被缓存, importScripts()的脚本按照各自的cache-control
决定all
,无特殊待遇,按照cache-control
头部决定是否缓存none
, 顶级服务脚本和importScripts()的脚本都不会被缓存navigator.serviceWorker.register('/serviceWorker.js', {
updateViaCache: 'none'
});
update()
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')
这种拦截能力不限于 fetch()方法发送的请求,也能拦截对 JavaScript、CSS、图片和HTML(包括对主 HTML 文档本身)等资源发送的请求。
FetchEvent 继承自 ExtendableEvent。让服务工作者线程能够决定如何处理 fetch 事件的方法是 event.respondWith()
。
self.onfetch = (fetchEvent) => {
fetchEvent.responseWith(fetch(fetchEvent.request));
}
self.onfetch = (fetchEvent) => {
fetchEvent.respondWith(caches.match(fetchEvent.request));
};
self.onfetch = (fetchEvent) => {
fetchEvent.respondWith(
fetch(fetchEvent.request)
.catch(() => caches.match(fetchEvent.request))
);
};
self.onfetch = (fetchEvent) => {
fetchEvent.respondWith(
caches.match(fetchEvent.request)
.then((response) => response || fetch(fetchEvent.request))
);
};
self.onfetch = (fetchEvent) => {
fetchEvent.respondWith(
// 开始执行“从缓存返回,以网络为后备”策略
caches.match(fetchEvent.request)
.then((response) => response || fetch(fetchEvent.request))
.catch(() => caches.match('/fallback.html'))
);
};
为了在 PWA 应用程序中支持推送通知,必须支持以下 4 种行为。
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
Notification.requestPermission()
.then((status) => {
if (status === 'granted') {
registration.showNotification('foo');
}
});
});
// 服务工作者内部也可
self.onactivate = () => self.registration.showNotification('bar');
self.onnotificationclick = ({notification}) => {
console.log('notification click', notification);
};
self.onnotificationclose = ({notification}) => {
console.log('notification close', notification);
};
navigator.serviceWorker.register('./serviceWorker.js')
.then((registration) => {
registration.pushManager.subscribe({
applicationServerKey: key, // 来自服务器的公钥
userVisibleOnly: true
});
});
// 服务工作者内部
self.onactivate = () => {
self.registration.pushManager.subscribe({
applicationServerKey: key, // 来自服务器的公钥
userVisibleOnly: true
});
};
/*************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');
};