虽万物今是而昨非,但昨日之我亦是今日之我 --《心理学通识》
大家好,我是柒八九
。
前天在Web性能优化之Worker线程(上)中针对Worker
中的{专用工作线程|Dedicated Worker}做了简单介绍和描述了如何配合webpack
在项目中使用。
今天,我们就着重对{服务工作线程|Service Worker}进行介绍。由于,在实际项目中,还未做实践,所以有些东西更偏向于概念和API的描述。但是,我感觉针对服务工作线程在项目优化方面还是有很大的可探索的空间的。
那我们就闲话少叙,开车走起。
由于该篇是介绍性文章,难免有一些比较生硬的概念。为了减轻大家的阅读负担,我用⭐️来标注,推荐阅读的篇幅。(5⭐️最高)
文章概要
- {服务工作线程|Service Worker}
- 基础概念 ⭐️⭐️⭐️
- 线程缓存 ⭐️⭐️⭐️⭐️
- 线程客户端
- 生命周期 ⭐️⭐️⭐️
- 控制反转与线程持久化
updateViaCache
管理服务文件缓存 ⭐️⭐️⭐️- 线程消息 ⭐️⭐️⭐️
- 拦截 fetch 事件 ⭐️⭐️⭐️⭐️⭐️
1. {服务工作线程|Service Worker}
{服务工作线程|Service Worker}是一种类似浏览器中代理服务器的线程,可以拦截外出请求和缓存响应。这可以让网页在没有网络连接的情况下正常使用,因为部分或全部页面可以从服务工作线程缓存中提供服务。
与共享工作线程类似,来自一个域的多个页面共享一个服务工作线程
服务工作线程在两个主要任务上最有用:
- 充当网络请求的缓存层
- 启用推送通知
在某种意义上
- 服务工作线程就是用于把网页变成像原生应用程序一样的工具
- 服务工作线程对大多数主流浏览器而言就是网络缓存
基础概念
作为一种工作线程,服务工作线程与专用工作线程和共享工作线程拥有很多共性。比如,在独立上下文中运行,只能通过异步消息通信。
ServiceWorkerContainer
服务工作线程与专用工作线程或共享工作线程的一个区别是没有全局构造函数。服务工作线程是通过 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
接口是浏览器对服务工作线程生态的顶部封装
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
对象。 - 该Promise不会拒绝
controller
:
返回与当前页面关联的激活的ServiceWorker
对象,如果没有激活的服务工作线程则返回null
。
ServiceWorkerContainer
支持下列方法
register()
:
使用接收的url
和options
对象创建或更新ServiceWorkerRegistration
getRegistration()
:返回Promise
- 成功时候返回与提供的作用域匹配的
ServiceWorkerRegistration对象
- 如果没有匹配的服务工作线程则返回
undefined
getRegistrations()
:返回Promise
- 成功时候返回与
ServiceWorkerContainer
关联的ServiceWorkerRegistration
对象的数组; - 如果没有关联的服务工作者线程则返回空数组。
startMessage()
:开始传送通过Client.postMessage()
派发的消息
使用 ServiceWorkerRegistration 对象
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 对象
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 复制代码
Cache 对象
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 }); 复制代码
options 对象
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 种服务工作者线程可能存在的状态:
- {已解析|parsed }
- {安装中|installing }
- {已安装|installed }
- {激活中|activating }
- {已激活|activated }
- {已失效|redundant }
上述状态的每次变化都会在 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
值。
浏览器获取脚本文件,然后执行一些初始化任务,服务工作线程的生命周期就开始了。
- (1) 确保服务脚本来自相同的源。
- (2) 确保在安全上下文中注册服务工作线程。
- (3) 确保服务脚本可以被浏览器 JavaScript 解释器成功解析而不会抛出任何错误。
- (4) 捕获服务脚本的快照。下一次浏览器下载到服务脚本,会与这个快照对比差异,并据此决定是否应该更新服务工作线程。
所有这些任务全部成功,则 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 处于等待中'); } }); 复制代码
激活中状态
激活中状态表示服务工作线程已经被浏览器选中即将变成可以控制页面的服务工作线程
如果浏览器中没有活动服务工作者线程,这个新服务工作者线程会自动到达激活中状态。如果有一个活动服务工作者线程,则这个作为替代的服务工作线程可以通过如下方式进入激活中状态。
- 原有服务工作线程控制的客户端数量变为 0。
这通常意味着所有受控的浏览器标签页都被关 闭。在下一个导航事件时,新服务工作线程会到达激活中状态。 - 已安装的服务工作者线程调用
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,也可以通过 </code>、<code><link></code>或<code><img></code>标签创建。</div><div>让服务工作线程能够决定如何处理 <code>fetch</code> 事件的方法是 <code>event.respondWith(</code>)。该方法接收Promise,该Promise会解决为一个 <code>Response</code> 对象。该 <code>Response</code>对象实际上来自哪里完全由服务工作线程决定。可以来自<strong>网络</strong>,来自<strong>缓存</strong>,或者<strong>动态创建</strong>。</div><h2 id="BMUKw"><br /></h2><h2 id="vdBK3">从网络返回</h2><div><br /></div><blockquote style="background-color: #FFF9F9;"><div>这个策略就是<strong>简单地转发</strong> <code>fetch</code> 事件</div></blockquote><div>那些绝对<strong>需要发送到服务器的请求</strong>例如 <code>POST</code> 请求就适合该策略。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22self.onfetch%20%3D%20(fetchEvent)%20%3D%3E%20%7B%5Cn%20fetchEvent.respondWith(fetch(fetchEvent.request))%3B%5Cn%7D%3B%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22aKvW9%22%7D"></div><h2 id="s9zyU">从缓存返回</h2><blockquote style="background-color: #FFF9F9;"><div>这个策略其实就是<strong>缓存检查</strong></div></blockquote><div>对于任何肯定有缓存的资源(如在安装阶段缓存的资源),可以采用该策略。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22self.onfetch%20%3D%20(fetchEvent)%20%3D%3E%20%7B%5Cn%20fetchEvent.respondWith(caches.match(fetchEvent.request))%3B%5Cn%7D%3B%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22BHcR9%22%7D"></div><h2 id="WxkHh"><br /></h2><h2 id="yBWwb">从网络返回,缓存作后备</h2><div><br /></div><div>这个策略把<strong>从网络获取最新的数据作为首选</strong>,但如果<strong>缓存中有值</strong>也会返回缓存的值。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22self.onfetch%20%3D%20(fetchEvent)%20%3D%3E%20%7B%5Cn%20fetchEvent.respondWith(%5Cn%20%20%20%20%20fetch(fetchEvent.request)%5Cn%20%20%20%20%20.catch(()%20%3D%3E%20caches.match(fetchEvent.request))%5Cn%20%20)%3B%5Cn%7D%3B%20%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22zxI1c%22%7D"></div><h2 id="v0imr"><br /></h2><h2 id="pgDpl">从缓存返回,网络作后备</h2><div><br /></div><div><br /></div><div>这个策略<strong>优先考虑响应速度</strong>,但仍会在没有缓存的情况下发送网络请求。这是大多数<strong>渐进式 Web 应用程序</strong>(PWA,Progressive Web Application)采取的<strong>首选策略</strong>。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22self.onfetch%20%3D%20(fetchEvent)%20%3D%3E%20%7B%5Cn%20%20%20fetchEvent.respondWith(%5Cn%20%20%20%20%20%20%20caches.match(fetchEvent.request)%5Cn%20%20%20%20%20%20%20.then((response)%20%3D%3E%20response%20%7C%7C%20fetch(fetchEvent.request))%5Cn%20%20%20)%3B%5Cn%7D%3B%20%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22PlM6K%22%7D"></div><h2 id="dkIUR"><br /></h2><h2 id="ozRN4">通用后备</h2><div><br /></div><div>应用程序需要考虑<strong>缓存和网络都不可用的情况</strong>。服务工作线程可以<strong>在安装时缓存后备资源</strong>,然后在缓存和网络都失败时返回它们。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22self.onfetch%20%3D%20(fetchEvent)%20%3D%3E%20%7B%5Cn%20%20%20fetchEvent.respondWith(%5Cn%20%20%20%20%20%2F%2F%20%E5%BC%80%E5%A7%8B%E6%89%A7%E8%A1%8C%E2%80%9C%E4%BB%8E%E7%BC%93%E5%AD%98%E8%BF%94%E5%9B%9E%EF%BC%8C%E4%BB%A5%E7%BD%91%E7%BB%9C%E4%B8%BA%E5%90%8E%E5%A4%87%E2%80%9D%E7%AD%96%E7%95%A5%5Cn%20%20%20%20%20caches.match(fetchEvent.request)%5Cn%20%20%20%20%20%20%20.then((response)%20%3D%3E%20response%20%7C%7C%20fetch(fetchEvent.request))%5Cn%20%20%20%20%20%20%20.catch(()%20%3D%3E%20caches.match('%2Ffallback.html'))%5Cn%20)%3B%5Cn%7D%3B%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22O6ywF%22%7D"></div><div><code>catch()</code>子句可以扩展为<strong>支持不同类型的后备</strong>。</div><h1 id="wihNb" style="text-align: center;"><br /></h1><h1 id="4bVvV" style="text-align: center;">后记</h1><div style="text-align: center;"><br /></div><div><strong>分享是一种态度</strong>。</div><div>参考文献:JS高级程序设计第四版</div><div><strong>全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。</strong></div><div><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_662814650dc749879619cd36bbc627de.gif%22%2C%22originWidth%22%3A413%2C%22originHeight%22%3A390%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A413%2C%22height%22%3A390%7D"></span></div><div><br /></div>