大家好,我是柒八九
。
前天在Web性能优化之Worker线程(上)中针对Worker
中的专用工作线程Dedicated Worker做了简单介绍和描述了如何配合webpack
在项目中使用。
今天,我们就着重对服务工作线程Service Worker进行介绍。由于,在实际项目中,还未做实践,所以有些东西更偏向于概念和API的描述。但是,我感觉针对「服务工作线程」在项目优化方面还是有很大的可探索的空间的。
那我们就闲话少叙,开车走起。
由于该篇是介绍性文章,难免有一些比较生硬的概念。为了减轻大家的阅读负担,我用⭐️来标注,推荐阅读的篇幅。(5⭐️最高)
updateViaCache
管理服务文件缓存 ⭐️⭐️⭐️服务工作线程Service Worker是一种类似浏览器中「代理服务器」的线程,可以「拦截外出请求」和「缓存响应」。这可以让网页在「没有网络连接」的情况下正常使用,因为部分或全部页面可以从服务工作线程缓存中提供服务。
❝与共享工作线程类似,来自「一个域」的多个页面「共享」一个服务工作线程 ❞
服务工作线程在两个主要任务上最有用:
❝在某种意义上
❞
作为一种「工作线程」,服务工作线程与专用工作线程和共享工作线程拥有很多共性。比如,在「独立上下文中」运行,只能通过「异步消息通信」。
服务工作线程与专用工作线程或共享工作线程的一个「区别」是「没有全局构造函数」。服务工作线程是通过 ServiceWorkerContainer
来管理的,它的实例保存在 navigator.serviceWorker
属性中。
该对象是个「顶级接口」,通过它可以让浏览器「创建」、「更新」、「销毁」或者与服务工作线程交互。
console.log(navigator.serviceWorker);
// ServiceWorkerContainer { ... }
ServiceWorkerContainer
「没有通过全局构造函数创建」,而是暴露了 register()
方法,该方法以与 Worker()
或 SharedWorker()
构造函数相同的方式传递「脚本 URL」。
serviceWorker.js
// 处理相关逻辑
main.js
navigator.serviceWorker.register('./serviceWorker.js');
❝
register()
方法返回一个Promise
Promise
成功时返回 ServiceWorkerRegistration
对象❞
serviceWorker.js
// 处理相关逻辑
main.js
// 注册成功,成功回调(解决)
navigator.serviceWorker.register('./serviceWorker.js')
.then(console.log, console.error);
// ServiceWorkerRegistration { ... }
// 使用不存在的文件注册,失败回调(拒绝)
navigator.serviceWorker.register('./doesNotExist.js')
.then(console.log, console.error);
// TypeError: Failed to register a ServiceWorker:
// A bad HTTP response code (404) was received
// when fetching the script.
即使浏览器「未全局支持」服务工作线程,服务工作线程本身对页面也应该是「不可见」的。这是因为它的行为类似代理,就算有需要它处理的操作,也仅仅是「发送常规的网络请求」。
考虑到上述情况,「注册」服务工作线程的一种非常常见的模式是「基于特性检测」,并在页面的 load
事件中操作。
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('./serviceWorker.js');
});
}
❝如果没有
load
事件做检测,服务工作线程的注册就会与「页面资源的加载重叠」,进而拖慢初始页面渲染的过程 ❞
❝
ServiceWorkerContainer
接口是浏览器对服务工作线程生态的「顶部封装」 ❞
ServiceWorkerContainer
「始终」可以在「客户端上下文」中访问:
console.log(navigator.serviceWorker);
// ServiceWorkerContainer { ... }
ServiceWorkerContainer
支持以下「事件处理程序」
oncontrollerchange
:
在 ServiceWorkerContainer
触发 controllerchange
事件时会调用指定的事件处理程序。ServiceWorkerRegistration
时触发。navigator.serviceWorker.addEventListener('controllerchange',handler)
处理。onerror
:
在关联的服务工作线程触发 ErrorEvent
错误事件时会调用指定的事件处理程序。navigator.serviceWorker.addEventListener('error', handler)
处理onmessage
:
在服务工作线程触发 MessageEvent
事件时会调用指定的事件处理程序navigator.serviceWorker.addEventListener('message', handler)
处理ServiceWorkerContainer
支持下列「属性」
ready
:返回 Promise
ServiceWorkerRegistration
对象。controller
:
返回与「当前页面关联」的激活的 ServiceWorker
对象,如果没有激活的服务工作线程则返回 null
。ServiceWorkerContainer
支持下列「方法」
register()
:
使用接收的 url
和 options
对象创建或更新 ServiceWorkerRegistration
getRegistration()
:返回 Promise
ServiceWorkerRegistration对象
undefined
getRegistrations()
:返回 Promise
ServiceWorkerContainer
关联的 ServiceWorkerRegistration
对象的「数组」;startMessage()
:开始传送通过 Client.postMessage()
派发的消息ServiceWorkerRegistration
对象表示「注册成功的」服务工作线程。该对象可以在 register()
返回的「解决Promise」的处理程序中访问到。通过它的一些属性可以确定关联服务工作线程的「生命周期状态」。
调用 navigator.serviceWorker.register()
之后返回的Promise会将注册成功的 ServiceWorkerRegistration
对象(注册对象)发送给处理函数。
❝在「同一页面」使用「同一 URL」 多次调用该方法会「返回相同的注册对象」:即该操作是「幂等」的 ❞
navigator.serviceWorker.register('./sw1.js')
.then((registrationA) => {
console.log(registrationA);
navigator.serviceWorker.register('./sw2.js')
.then((registrationB) => {
console.log(registrationA === registrationB);
// 这里结果为true
});
});
ServiceWorkerRegistration
支持以下「事件处理程序」
onupdatefound
:
在服务工作线程触发 updatefound
事件时会调用指定的事件处理程序。ServiceWorkerRegistration.installing
收到一个新的服务工作者线程serviceWorkerRegistration.addEventListener('updatefound',handler)
处理ServiceWorkerRegistration
支持以下「通用属性」
scope
:
1. 返回服务工作线程作用域的「完整 URL 路径」
2. 该值源自接收服务脚本的路径和在register()中提供的作用域navigationPreload
:
返回与注册对象关联的 NavigationPreloadManager
实例pushManager
:
返回与注册对象关联的 pushManager
实例ServiceWorkerRegistration
还支持以下「属性」,可用于判断服务工作者线程处于「生命周期」的什么阶段。
installing
:
如果有则返回状态为 installing
(安装)的服务工作者线程,否则为 null。waiting
:
如果有则返回状态为 waiting
(等待)的服务工作者线程,否则为 null。active
:
如果有则返回状态 activating
或 active(活动)的服务工作者线程,否则为 null❝这些属性都是服务工作线程状态的「一次性快照」 ❞
ServiceWorkerRegistration
支持下列「方法」
getNotifications()
:
返回Promise,解决为 Notification
「对象的数组」。showNotifications()
:
显示通知,可以配置 title
和 options
参数。update()
:
直接从服务器重新请求服务脚本,如果新脚本不同,则重新初始化。unregister()
:
「取消」服务工作线程的注册。该方法会在服务工作线程「执行完再取消注册」。❝服务工作线程也「受加载脚本对应源的常规限制」 ❞
此外,由于服务工作线程几乎可以「任意修改和重定向网络请求」,以及加载静态资源,服务工作者线程 API 「只能在安全上下文(HTTPS)下使用」。在非安全上下文(HTTP)中,navigator.serviceWorker
是 undefined
。
❝服务工作线程「只能拦截其作用域内」的客户端发送的请求 ❞
「作用域是相对于获取服务脚本的路径定义的」。如果没有在 register()
中指定,则作用域就是服务脚本的路径。
wl.js
在https://wl.com/
作用域内
navigator.serviceWorker
.register('/wl.js')
.then((serviceWorkerRegistration) => {
console.log(serviceWorkerRegistration.scope);
// https://wl.com/
});
// 以下请求都会被拦截:
// fetch('/foo.js');
// fetch('/foo/fooScript.js');
// fetch('/baz/bazScript.js');
navigator.serviceWorker
.register('/wl.js', {scope: './'})
.then((serviceWorkerRegistration) => {
console.log(serviceWorkerRegistration.scope);
// https://wl.com/
});
// 以下请求都会被拦截:
// fetch('/foo.js');
// fetch('/foo/fooScript.js');
// fetch('/baz/bazScript.js');
navigator.serviceWorker
.register('/wl.js', {scope: './foo'})
.then((serviceWorkerRegistration) => {
console.log(serviceWorkerRegistration.scope);
// https://wl.com/foo/
});
// 以下请求都会被拦截:
// fetch('/foo/fooScript.js');
// 以下请求都不会被拦截:
// fetch('/foo.js');
// fetch('/baz/bazScript.js');
navigator.serviceWorker
.register('/foo/wl.js')
.then((serviceWorkerRegistration) => {
console.log(serviceWorkerRegistration.scope);
// https://wl.com/foo/
});
// 以下请求都会被拦截:
// fetch('/foo/fooScript.js');
// 以下请求都不会被拦截:
// fetch('/foo.js');
// fetch('/baz/bazScript.js');
❝服务工作线程的作用域实际上遵循了「目录权限模型」,即只能相对于服务脚本所在路径「缩小作用域」 ❞
❝服务工作线程的一个主要能力是可以「通过编程方式实现真正的网络请求缓存机制」 ❞
有如下特点:
LRU
,Least RecentlyUsed)原则为新缓存腾出空间
关于LRU
我们在网络拾遗之Http缓存中有介绍❝本质上,服务工作线程缓存机制是一个「双层字典」,其中「顶级」字典的条目映射到二级嵌套字典 ❞
顶级字典是 CacheStorage
对象,可以通过服务工作线程全局作用域的 caches
属性访问。顶级字典中的每个值都是一个 Cache
对象,该对象也是个「字典」,是 Request
对象到 Response
对象的映射。
❝
CacheStorage
对象是映射到Cache
对象的字符串「键/值存储」 ❞
CacheStorage
提供的 API 类似于「异步 Map」。CacheStorage
的接口通过全局对象的 caches
属性暴露出来。
console.log(caches); // CacheStorage {}
CacheStorage
中的每个缓存可以通过给 caches.open()
传入相应「字符串键」取得。
❝
Cache
对象是通过Promise返回 ❞
caches.open('v1').then(console.log);
// Cache {}
与 Map 类似,CacheStorage
也有 has()
、delete()
和 keys()
方法,他们都返回Promise
// 打开新缓存 v1
caches.open('v1')
// 检查缓存 v1 是否存在
.then(() => caches.has('v1'))
.then(console.log) // true
// 检查不存在的缓存 v2
.then(() => caches.has('v2'))
.then(console.log); // false
// 打开缓存 v1、v3 和 v2
caches.open('v1')
.then(() => caches.open('v3'))
.then(() => caches.open('v2'))
// 检查当前缓存的键
.then(() => caches.keys())
// 缓存键按创建顺序输出
.then(console.log); // ["v1", "v3", "v2"]
CacheStorage
接口还有一个 match()
方法,可以根据 Request
对象搜索 CacheStorage
中的「所有」 Cache
对象
// 创建一个请求键和两个响应值
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
CacheStorage
通过字符串映射到 Cache
对象。Cache
对象跟 CacheStorage
一样,类似于「异步 Map」。
Cache
键可以是 URL 字符串
,也可以是 Request 对象
。这些键会「映射」到 Response
对象。
❝服务工作线程缓存「只考虑缓存
HTTP
的GET
请求」 ❞
为填充 Cache
,可能使用以下三个方法
put(request, response)
:
1. 在键(Request 对象或 URL 字符串)和值(Response 对象)「同时存在」时用于添加缓存项
2. 该方法「返回Promise」,在添加成功后会解决add(request)
:
1. 在只有 Request 对象或 URL 时使用此方法发送 fetch()
请求,并缓存响应。
2. 该方法返回Promise,Promise在添加成功后会解决addAll(requests)
:
1. 在希望「填充全部缓存时」使用,比如在服务工作线程「初始化时」也初始化缓存
2. 该方法接收 URL 或 Request 对象的「数组」
3. addAll()
会对请求数组中的「每一项分别调用」add()
4. 该方法返回Promise,Promise在所有缓存内容添加成功后会解决。与 Map 类似,Cache
也有 delete()
和 keys()
方法。但都基于Promise。
const request1 = new Request('https://www.wl.com');
const response1 = new Response('fooResponse');
caches.open('v1')
.then((cache) => {
cache.put(request1, response1)
.then(() => cache.keys())
.then(console.log) // [Request]
.then(() => cache.delete(request1))
.then(() => cache.keys())
.then(console.log); // []
});
❝「缓存是否命中」取决于
URL 字符串
和Request 对象 URL
两者的一种 是否匹配 ❞
URL 字符串和 Request 对象是「可互换」的,因为匹配时会提取 Request 对象的 URL。
const request1 = 'https://www.wl.com';
const request2 = new Request('https://www.bar.com');
const response1 = new Response('fooResponse');
const response2 = new Response('barResponse');
caches.open('v1').then((cache) => {
cache.put(request1, response1)
.then(() => cache.put(request2, response2))
.then(() => cache.match(new Request('https://www.foo.com')))
.then((response) => response.text())
.then(console.log) // fooResponse
.then(() => cache.match('https://www.bar.com'))
.then((response) => response.text())
.then(console.log); // barResponse
});
Cache.match()
、Cache.matchAll()
和 CacheStorage.match()
都支持可选的 options 对象,它允许通过设置以下属性来配置 URL 匹配的行为
cacheName
:
只有 CacheStorage.matchAll()
支持。设置为字符串时,只会匹配 Cache
键为指定字符串的缓存值ignoreSearch
:
1. 设置为 true 时,在匹配 URL 时「忽略查询字符串」,包括请求查询和缓存键。
2. 例如,https://example.com?foo=bar
会匹配 https://example.com
ignoreMethod
:
1. 设置为 true 时,在匹配 URL 时忽略请求查询的 HTTP 方法ignoreVary
:
1. 匹配的时候考虑 HTTP 的 Vary
头部,该头部指定哪个请求头部导致服务器响应不同的值。
2. ignoreVary 设置为 true 时,在匹配 URL 时忽略 Vary 头部使用 StorageEstimate
API 可以近似地获悉有多少空间可用(以字节为单位),以及当前使用了多少空间
navigator.storage.estimate()
.then(console.log);
服务工作线程会使用 Client
对象跟踪「关联的窗口」、「工作线程」或「服务工作线程」。服务工作线程可以通过 Clients
接口访问这些 Client
对象。该接口暴露在「全局上下文」的 self.clients
属性上。
Client 对象支持以下属性和方法。
id
:
1. 返回客户端的「全局唯一标识符」
2. id可用于通过 Client.get()
获取客户端的引用type
:
1. 返回表示「客户端类型」的字符串。
2. type 可能的值是 window
、worker
或 sharedworker
url
:
返回客户端的 URLpostMessage()
:
用于向「单个」客户端发送消息Clients
接口也支持以下方法
openWindow(url)
:
1. 在新窗口中打开指定 URL,实际上会给当前服务工作线程添加一个「新Client」
2. 这个新 Client 对象以解决的Promise形式返回。
3. 该方法可用于回应点击通知的操作,此时服务工作线程可以检测单击事件并作为响应打开一个窗口claim()
:
1. 强制性设置当前服务工作线程以控制其作用域中的所有客户端。
2. claim()可用于「不希望等待页面重新加载」而让服务工作线程开始管理页面Service Worker 规范定义了 6 种服务工作者线程可能存在的状态:
上述状态的「每次变化」都会在 ServiceWorker
对象上触发 statechange
事件。
navigator.serviceWorker
.register('./serviceWorker.js')
.then((registration) => {
registration
.installing
.onstatechange = ({ target: { state } }) => {
console.log('state changed to', state);
};
});
调用 navigator.serviceWorker.register()
会「启动创建」服务工作线程实例的过程。刚创建的服务工作线程实例会进入「已解析状态」。该状态「没有事件」,也「没有」与之相关的 ServiceWorker.state
值。
浏览器获取脚本文件,然后执行一些「初始化任务」,服务工作线程的生命周期就开始了。
所有这些任务全部成功,则 register()
返回的Promise会解决为一个 ServiceWorkerRegistration
对象。新创建的服务工作者线程实例「进入到安装中状态」。
「安装中状态」是执行「所有」服务工作线程设置任务的状态。这些任务包括在服务工作线程控制页面前必须完成的操作。
在客户端,这个阶段可以通过「检查」 ServiceWorkerRegistration.installing
是否被设置为 ServiceWorker
实例:
navigator.serviceWorker
.register('./serviceWorker.js')
.then((registration) => {
if (registration.installing) {
console.log('Service worker 处于安装中状态');
}
});
「关联」的 ServiceWorkerRegistration
对象也会在服务工作线程到达该状态时触发 updatefound
事件
navigator.serviceWorker
.register('./serviceWorker.js')
.then((registration) => {
registration.onupdatefound = () =>
console.log('Service worker 处于安装中状态');
};
});
在「服务工作线程中」,这个阶段可以通过给 install
事件添加处理程序来确定:
self.oninstall = (installEvent) => {
console.log('Service worker 处于安装中状态');
};
安装中状态「频繁」用于「填充服务工作线程的缓存」。服务工作线程在「成功缓存指定资源之前」可以「一直处于该状态」。
服务工作线程可以通过 ExtendableEvent
停留在安装中状态。
延迟 5 秒再将状态过渡到已安装状态
self.oninstall = (installEvent) => {
installEvent.waitUntil(
new Promise((resolve, reject)
=> setTimeout(resolve, 5000))
);
};
通过 Cache.addAll()
缓存一组资源之后再过渡
const CACHE_KEY = 'v1';
self.oninstall = (installEvent) => {
installEvent.waitUntil(
caches.open(CACHE_KEY)
.then((cache) => cache.addAll([
'foo.js',
'bar.html',
'baz.css',
]))
);
};
已安装状态也称为「等待中」(waiting)状态,意思是服务工作线程此时没有别的事件要做,只是准 备在得到许可的时候去控制客户端。
如果没有「活动的」服务工作线程,则新安装的服务工作者线程会跳 到这个状态,并直接进入激活中状态,因为没有必要再等了。
在「客户端」,这个阶段可以通过检查 ServiceWorkerRegistration.waiting
是否被设置为一个 ServiceWorker
实例来确定:
navigator.serviceWorker
.register('./serviceWorker.js')
.then((registration) => {
if (registration.waiting) {
console.log('Service worker 处于等待中');
}
});
❝「激活中状态」表示服务工作线程已经被浏览器选中即将变成可以控制页面的服务工作线程 ❞
如果浏览器中没有活动服务工作者线程,这个新服务工作者线程会「自动」到达激活中状态。如果有一个活动服务工作者线程,则这个作为替代的服务工作线程可以通过如下方式进入激活中状态。
self.skipWaiting()
。
这样可以「立即生效」,而不必等待一次导航事件在「客户端」,这个阶段大致可以通过检查 ServiceWorkerRegistration.active
是否被设置为一个 ServiceWorker 实例来确定:
navigator.serviceWorker
.register('./serviceWorker.js')
.then((registration) => {
if (registration.active) {
console.log('Service worker 处于激活中');
}
});
在这个服务工作线程「内部」,可以通过给 activate
事件添加处理程序来获悉
self.oninstall = (activateEvent) => {
console.log('Service worker 处于激活中');
};
❝
activate
事件表示可以将「老服务工作线程清理掉」了,该事件经常用于「清除旧缓存数据和迁移数据库」 ❞
const CACHE_KEY = 'v3';
self.oninstall = (activateEvent) => {
caches.keys()
.then((keys) =>
keys.filter((key) => key != CACHE_KEY))
.then((oldKeys) =>
oldKeys.forEach((oldKey) => caches.delete(oldKey));
};
「已激活状态」表示服务工作线程「正在控制」一个或多个客户端。在这个状态,服务工作线程会捕获 其作用域中的 「fetch()事件」、「通知和推送事件」。
在「客户端」,这个阶段「大致」可以通过检查 ServiceWorkerRegistration.active
是否被设置为一个 ServiceWorker 实例来确定:
navigator.serviceWorker
.register('./serviceWorker.js')
.then((registration) => {
if (registration.active) {
console.log('Service worker 已激活');
}
});
「更可靠」的确定服务工作线程处于「已激活状态」一种方式是检查 ServiceWorkerRegistration
的 controller
属性。该属性会返回激活的 ServiceWorker
实例,即「控制页面的实例」:
navigator.serviceWorker
.register('./serviceWorker.js')
.then((registration) => {
if (registration.controller) {
console.log('Service worker 已激活');
}
});
在「新服务工作线程控制客户端」时,该客户端中的 ServiceWorkerContainer
会触发 controllerchange
事件:
navigator.serviceWorker.oncontrollerchange = () => {
console.log('新的服务线程正在控制客户端');
};
「已失效状态」表示服务工作线程「已被宣布死亡」。不会再有事件发送给它,浏览器随时可能销毁它并回收它的资源。
❝服务工作者线程遵循控制反转Inversion of Control(IOC)模式并且是「事件驱动」的 ❞
意味着服务工作线程「不应该依赖」工作线程的全局状态。服务工作者线程中的绝大多数代码应该在「事件处理程序」中定义。
大多数浏览器将服务工作线程实现为「独立的进程」,而该进程「由浏览器单独控制」。如果浏览器检测到某个服务工作线程空闲了,就可以终止它并在需要时再重新启动。这意味着可以「依赖」服务工作线程在「激活后处理事件」,但不能依赖它们的持久化全局状态。
updateViaCache
管理服务文件缓存正常情况下,浏览器加载的「所有 JS 资源」会按照它们的 Cache-Control
头部「纳入 HTTP 缓存管理」。因为服务脚本「没有优先权」,所以浏览器不会在缓存文件「失效前」接收更新的服务脚本。
为了尽可能传播更新后的服务脚本,常见的解决方案是在服务端端「响应脚本请求时」设置 Cache-Control:max-age=0
头部。这样浏览器就能「始终取得最新的脚本文件」。
这个「即时失效的」方案能够满足需求,但仅仅依靠 HTTP 头部来决定是否更新意味着「只能由服务器控制客户端」。
为了「让客户端能控制自己的更新行为」,可以通过 updateViaCache
属性设置「客户端对待服务脚本的方式」。
该属性可以在「注册」服务工作线程时定义,对应的值如下:
imports
:
1. 「默认值」
2. 顶级服务脚本「永远不会被缓存」,但通过 importScripts()
在服务工作线程内部导入的文件会按照 Cache-Control
头部设置纳入 HTTP 缓存管理all
:
1. 服务脚本「没有任何特殊待遇」
2. 所有文件都会按照 Cache-Control
头部设置纳入 HTTP 缓存管理none
:
1. 顶级服务脚本和通过 importScripts()
在服务工作线程内部导入的文件「永远都不会被缓存」navigator.serviceWorker
.register('/serviceWorker.js', {
updateViaCache: 'none'
});
❝服务工作线程也能与客户端通过
postMessage()
交换消息 ❞
实现通信的最简单方式是向活动工作线程发送一条消息,然后使用「事件对象」发送回应。发送给服务工作线程的消息可以在「全局作用域处理」,而发送回客户端的消息则可以在 ServiceWorkerContext
对象上处理。
// main.js
navigator.serviceWorker
.register('./serviceWorker.js')
.then((registration) => {
if (registration.active) {
registration.active.postMessage('foo');
}
});
navigator.serviceWorker.onmessage = ({data}) => {
console.log('客户端收到消息:', data);
};
=======
// ServiceWorker.js
self.onmessage = ({data, source}) => {
console.log('线程收到消息:', data);
source.postMessage('bar');
};
输出结果
// 线程收到消息: foo
// 客户端收到消息: bar
serviceWorker.controller
属性main.js
navigator.serviceWorker.onmessage = ({data}) => {
console.log('客户端收到消息', data);
};
navigator.serviceWorker
.register('./serviceWorker.js')
.then(() => {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker
.controller.postMessage('foo');
}
});
====
ServiceWorker.js
self.onmessage = ({data, source}) => {
console.log('线程收到消息:', data);
source.postMessage('bar');
};
输出结果
// 线程收到消息: foo
// 客户端收到消息: bar
上面两个例子在每次「页面重新加载」时都会运行。这是因为服务工作线程会「回应每次刷新后」客户端脚本发送的消息。
ServiceWorker.js
self.onmessage = ({data}) => {
console.log('线程收到消息', data);
};
self.onactivate = () => {
self.clients
.matchAll({includeUncontrolled: true})
.then(
(clientMatches) =>
clientMatches[0].postMessage('foo')
);
};
======
main.js
navigator.serviceWorker.onmessage = ({data, source}) => {
console.log('客户端收到消息', data);
source.postMessage('bar');
};
navigator.serviceWorker.register('./serviceWorker.js')
输出结果
// 客户端收到消息: foo
// 线程收到消息 : bar
❝服务工作线程「最重要」的一个特性就是「拦截网络请求」 ❞
服务工作线程作用域中的「网络请求会注册为 fetch
事件」。这种拦截能力「不限于」 fetch()
方法发送的请求,也能拦截对 JavaScript
、CSS
、图片和HTML(包括对主 HTML 文档本身)等资源发送的请求。
这些请求可以来自 JavaScript,也可以通过 <script>
、<link>
或<img>
标签创建。
让服务工作线程能够决定如何处理 fetch
事件的方法是 event.respondWith(
)。该方法接收Promise,该Promise会解决为一个 Response
对象。该 Response
对象实际上来自哪里完全由服务工作线程决定。可以来自「网络」,来自「缓存」,或者「动态创建」。
❝这个策略就是「简单地转发」
fetch
事件 ❞
那些绝对「需要发送到服务器的请求」例如 POST
请求就适合该策略。
self.onfetch = (fetchEvent) => {
fetchEvent.respondWith(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))
);
};
这个策略「优先考虑响应速度」,但仍会在没有缓存的情况下发送网络请求。这是大多数「渐进式 Web 应用程序」(PWA,Progressive Web Application)采取的「首选策略」。
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'))
);
};
catch()
子句可以扩展为「支持不同类型的后备」。
参考资料:JS高级程序设计第四版