前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Web性能优化之Worker线程(下)

Web性能优化之Worker线程(下)

作者头像
前端柒八九
发布2022-08-25 15:04:38
2.5K0
发布2022-08-25 15:04:38
举报
文章被收录于专栏:柒八九技术收纳盒

大家好,我是柒八九

前天在Web性能优化之Worker线程(上)中针对Worker中的专用工作线程Dedicated Worker做了简单介绍和描述了如何配合webpack在项目中使用。

今天,我们就着重对服务工作线程Service Worker进行介绍。由于,在实际项目中,还未做实践,所以有些东西更偏向于概念和API的描述。但是,我感觉针对「服务工作线程」在项目优化方面还是有很大的可探索的空间的。

那我们就闲话少叙,开车走起。

由于该篇是介绍性文章,难免有一些比较生硬的概念。为了减轻大家的阅读负担,我用⭐️来标注,推荐阅读的篇幅。(5⭐️最高)

文章概要

  1. 服务工作线程Service Worker
  2. 基础概念 ⭐️⭐️⭐️
  3. 线程缓存 ⭐️⭐️⭐️⭐️
  4. 线程客户端
  5. 生命周期 ⭐️⭐️⭐️
  6. 控制反转与线程持久化
  7. updateViaCache 管理服务文件缓存 ⭐️⭐️⭐️
  8. 线程消息 ⭐️⭐️⭐️
  9. 拦截 fetch 事件 ⭐️⭐️⭐️⭐️⭐️

1.服务工作线程Service Worker

服务工作线程Service Worker是一种类似浏览器中「代理服务器」的线程,可以「拦截外出请求」「缓存响应」。这可以让网页在「没有网络连接」的情况下正常使用,因为部分或全部页面可以从服务工作线程缓存中提供服务。

❝与共享工作线程类似,来自「一个域」的多个页面「共享」一个服务工作线程 ❞

服务工作线程在两个主要任务上最有用:

  • 充当「网络请求的缓存层」
  • 启用「推送通知」

❝在某种意义上

  • 服务工作线程就是用于把网页变成像「原生应用程序」一样的「工具」
  • 服务工作线程对大多数主流浏览器而言就是「网络缓存」

基础概念

作为一种「工作线程」,服务工作线程与专用工作线程和共享工作线程拥有很多共性。比如,在「独立上下文中」运行,只能通过「异步消息通信」

ServiceWorkerContainer

服务工作线程与专用工作线程或共享工作线程的一个「区别」「没有全局构造函数」。服务工作线程是通过 ServiceWorkerContainer 来管理的,它的实例保存在 navigator.serviceWorker 属性中。

该对象是个「顶级接口」,通过它可以让浏览器「创建」「更新」「销毁」或者与服务工作线程交互。

代码语言:javascript
复制
console.log(navigator.serviceWorker);
// ServiceWorkerContainer { ... }

创建服务工作线程

ServiceWorkerContainer 「没有通过全局构造函数创建」,而是暴露了 register()方法,该方法以与 Worker()SharedWorker()构造函数相同的方式传递「脚本 URL」

代码语言:javascript
复制
serviceWorker.js
// 处理相关逻辑

main.js
navigator.serviceWorker.register('./serviceWorker.js');

register()方法返回一个Promise

  • Promise 成功时返回 ServiceWorkerRegistration 对象
  • 在注册失败时拒绝

代码语言:javascript
复制
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 事件中操作。

代码语言:javascript
复制
if ('serviceWorker' in navigator) {
 window.addEventListener('load', () => {
     navigator.serviceWorker
     .register('./serviceWorker.js');
 });
} 

❝如果没有 load 事件做检测,服务工作线程的注册就会与「页面资源的加载重叠」,进而拖慢初始页面渲染的过程 ❞

使用 ServiceWorkerContainer 对象

ServiceWorkerContainer 接口是浏览器对服务工作线程生态的「顶部封装」

ServiceWorkerContainer 「始终」可以在「客户端上下文」中访问:

代码语言:javascript
复制
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(): 使用接收的 urloptions 对象创建或更新 ServiceWorkerRegistration
  • getRegistration():返回 Promise
    • 成功时候返回与提供的作用域匹配的 ServiceWorkerRegistration对象
    • 如果没有匹配的服务工作线程则返回 undefined
  • getRegistrations():返回 Promise
    • 成功时候返回与 ServiceWorkerContainer 关联的 ServiceWorkerRegistration 对象的「数组」
    • 如果没有关联的服务工作者线程则返回空数组。
  • startMessage():开始传送通过 Client.postMessage()派发的消息

使用 ServiceWorkerRegistration 对象

ServiceWorkerRegistration 对象表示「注册成功的」服务工作线程。该对象可以在 register() 返回的「解决Promise」的处理程序中访问到。通过它的一些属性可以确定关联服务工作线程的「生命周期状态」

调用 navigator.serviceWorker.register()之后返回的Promise会将注册成功的 ServiceWorkerRegistration 对象(注册对象)发送给处理函数。

❝在「同一页面」使用「同一 URL」 多次调用该方法会「返回相同的注册对象」:即该操作是「幂等」的 ❞

代码语言:javascript
复制
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(): 显示通知,可以配置 titleoptions 参数。
  • update(): 直接从服务器重新请求服务脚本,如果新脚本不同,则重新初始化。
  • unregister()「取消」服务工作线程的注册。该方法会在服务工作线程「执行完再取消注册」

安全限制

❝服务工作线程也「受加载脚本对应源的常规限制」

此外,由于服务工作线程几乎可以「任意修改和重定向网络请求」,以及加载静态资源,服务工作者线程 API 「只能在安全上下文(HTTPS)下使用」。在非安全上下文(HTTP)中,navigator.serviceWorkerundefined

作用域限制

❝服务工作线程「只能拦截其作用域内」的客户端发送的请求 ❞

「作用域是相对于获取服务脚本的路径定义的」。如果没有在 register()中指定,则作用域就是服务脚本的路径。

通过「根目录」获取服务脚本对应的「默认根作用域」:

wl.jshttps://wl.com/作用域内

代码语言:javascript
复制
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');

通过「根目录」获取服务脚本但「指定同一目录」作用域

代码语言:javascript
复制
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'); 

通过「根目录」获取服务脚本但限定了「目录作用域」

代码语言:javascript
复制
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');

通过「嵌套的二级目录」获取服务脚本对应的「同一目录作用域」

代码语言:javascript
复制
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 属性暴露出来。

代码语言:javascript
复制
console.log(caches); // CacheStorage {}

CacheStorage 中的每个缓存可以通过给 caches.open()传入相应「字符串键」取得。

  • 非字符串键会转换为字符串
  • 如果缓存不存在,就会创建

Cache 对象是通过Promise返回 ❞

代码语言:javascript
复制
caches.open('v1').then(console.log);
// Cache {}

与 Map 类似,CacheStorage 也有 has()delete()keys()方法,他们都返回Promise

代码语言:javascript
复制
// 打开新缓存 v1
caches.open('v1')
  // 检查缓存 v1 是否存在
  .then(() => caches.has('v1'))
  .then(console.log) // true
  // 检查不存在的缓存 v2
  .then(() => caches.has('v2'))
  .then(console.log); // false
代码语言:javascript
复制
// 打开缓存 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 对象

代码语言:javascript
复制
// 创建一个请求键和两个响应值
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 对象。

❝服务工作线程缓存「只考虑缓存 HTTPGET 请求」

为填充 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。

代码语言:javascript
复制
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。

代码语言:javascript
复制
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 可以近似地获悉有多少空间可用(以字节为单位),以及当前使用了多少空间

代码语言:javascript
复制
navigator.storage.estimate()
  .then(console.log);

线程客户端

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

Client 对象支持以下属性和方法。

  • id: 1. 返回客户端的「全局唯一标识符」 2. id可用于通过 Client.get()获取客户端的引用
  • type: 1. 返回表示「客户端类型」的字符串。 2. type 可能的值是 windowworkersharedworker
  • url: 返回客户端的 URL
  • postMessage(): 用于向「单个」客户端发送消息

Clients 接口也支持以下方法

  • openWindow(url): 1. 在新窗口中打开指定 URL,实际上会给当前服务工作线程添加一个「新Client」 2. 这个新 Client 对象以解决的Promise形式返回。 3. 该方法可用于回应点击通知的操作,此时服务工作线程可以检测单击事件并作为响应打开一个窗口
  • claim(): 1. 强制性设置当前服务工作线程以控制其作用域中的所有客户端。 2. claim()可用于「不希望等待页面重新加载」而让服务工作线程开始管理页面

生命周期

Service Worker 规范定义了 6 种服务工作者线程可能存在的状态:

  1. 已解析parsed
  2. 安装中installing
  3. 已安装installed
  4. 激活中activating
  5. 已激活activated
  6. 已失效redundant

上述状态的「每次变化」都会在 ServiceWorker 对象上触发 statechange 事件。

代码语言:javascript
复制
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 实例:

代码语言:javascript
复制
navigator.serviceWorker
  .register('./serviceWorker.js')
  .then((registration) => {
     if (registration.installing) {
       console.log('Service worker 处于安装中状态');
   }
});

「关联」ServiceWorkerRegistration 对象也会在服务工作线程到达该状态时触发 updatefound事件

代码语言:javascript
复制
navigator.serviceWorker
  .register('./serviceWorker.js')
  .then((registration) => {
     registration.onupdatefound = () =>
       console.log('Service worker 处于安装中状态');
     };
}); 

「服务工作线程中」,这个阶段可以通过给 install 事件添加处理程序来确定:

代码语言:javascript
复制
self.oninstall = (installEvent) => {
 console.log('Service worker 处于安装中状态');
};

安装中状态「频繁」用于「填充服务工作线程的缓存」。服务工作线程在「成功缓存指定资源之前」可以「一直处于该状态」

服务工作线程可以通过 ExtendableEvent 停留在安装中状态。

延迟 5 秒再将状态过渡到已安装状态

代码语言:javascript
复制
self.oninstall = (installEvent) => {
   installEvent.waitUntil(
       new Promise((resolve, reject) 
         => setTimeout(resolve, 5000))
   );
}; 

通过 Cache.addAll()缓存一组资源之后再过渡

代码语言:javascript
复制
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 实例来确定:

代码语言:javascript
复制
navigator.serviceWorker
  .register('./serviceWorker.js')
  .then((registration) => {
     if (registration.waiting) {
       console.log('Service worker 处于等待中');
     }
}); 

激活中状态

「激活中状态」表示服务工作线程已经被浏览器选中即将变成可以控制页面的服务工作线程 ❞

如果浏览器中没有活动服务工作者线程,这个新服务工作者线程会「自动」到达激活中状态。如果有一个活动服务工作者线程,则这个作为替代的服务工作线程可以通过如下方式进入激活中状态。

  • 原有服务工作线程控制的客户端数量变为 0。 这通常意味着「所有受控」的浏览器标签页都被关 闭。在下一个「导航事件」时,新服务工作线程会到达激活中状态。
  • 「已安装」的服务工作者线程调用 self.skipWaiting()。 这样可以「立即生效」,而不必等待一次导航事件

「客户端」,这个阶段大致可以通过检查 ServiceWorkerRegistration.active 是否被设置为一个 ServiceWorker 实例来确定:

代码语言:javascript
复制
navigator.serviceWorker
  .register('./serviceWorker.js')
  .then((registration) => {
     if (registration.active) {
       console.log('Service worker 处于激活中');
     }
}); 

在这个服务工作线程「内部」,可以通过给 activate 事件添加处理程序来获悉

代码语言:javascript
复制
self.oninstall = (activateEvent) => {
 console.log('Service worker 处于激活中');
}; 

activate 事件表示可以将「老服务工作线程清理掉」了,该事件经常用于「清除旧缓存数据和迁移数据库」

代码语言:javascript
复制
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 实例来确定:

代码语言:javascript
复制
navigator.serviceWorker
  .register('./serviceWorker.js')
  .then((registration) => {
     if (registration.active) {
       console.log('Service worker 已激活');
     }
}); 

「更可靠」的确定服务工作线程处于「已激活状态」一种方式是检查 ServiceWorkerRegistrationcontroller 属性。该属性会返回激活的 ServiceWorker 实例,即「控制页面的实例」

代码语言:javascript
复制
navigator.serviceWorker
  .register('./serviceWorker.js')
  .then((registration) => {
     if (registration.controller) {
        console.log('Service worker 已激活');
     }
}); 

「新服务工作线程控制客户端」时,该客户端中的 ServiceWorkerContainer 会触发 controllerchange 事件:

代码语言:javascript
复制
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()在服务工作线程内部导入的文件「永远都不会被缓存」
代码语言:javascript
复制
navigator.serviceWorker
  .register('/serviceWorker.js', {
     updateViaCache: 'none'
  });

线程消息

❝服务工作线程也能与客户端通过 postMessage()交换消息 ❞

实现通信的最简单方式是向活动工作线程发送一条消息,然后使用「事件对象」发送回应。发送给服务工作线程的消息可以在「全局作用域处理」,而发送回客户端的消息则可以在 ServiceWorkerContext 对象上处理。

代码语言:javascript
复制
// 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 属性

代码语言:javascript
复制
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 

上面两个例子在每次「页面重新加载」时都会运行。这是因为服务工作线程会「回应每次刷新后」客户端脚本发送的消息。

线程率先发送消息

代码语言:javascript
复制
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 事件」。这种拦截能力「不限于」 fetch()方法发送的请求,也能拦截对 JavaScriptCSS、图片和HTML(包括对主 HTML 文档本身)等资源发送的请求。

这些请求可以来自 JavaScript,也可以通过 <script><link><img>标签创建。

让服务工作线程能够决定如何处理 fetch 事件的方法是 event.respondWith()。该方法接收Promise,该Promise会解决为一个 Response 对象。该 Response对象实际上来自哪里完全由服务工作线程决定。可以来自「网络」,来自「缓存」,或者「动态创建」

从网络返回

❝这个策略就是「简单地转发」 fetch 事件 ❞

那些绝对「需要发送到服务器的请求」例如 POST 请求就适合该策略。

代码语言:javascript
复制
self.onfetch = (fetchEvent) => {
 fetchEvent.respondWith(fetch(fetchEvent.request));
};

从缓存返回

❝这个策略其实就是「缓存检查」

对于任何肯定有缓存的资源(如在安装阶段缓存的资源),可以采用该策略。

代码语言:javascript
复制
self.onfetch = (fetchEvent) => {
 fetchEvent.respondWith(caches.match(fetchEvent.request));
};

从网络返回,缓存作后备

这个策略把「从网络获取最新的数据作为首选」,但如果「缓存中有值」也会返回缓存的值。

代码语言:javascript
复制
self.onfetch = (fetchEvent) => {
 fetchEvent.respondWith(
     fetch(fetchEvent.request)
     .catch(() => caches.match(fetchEvent.request))
  );
}; 

从缓存返回,网络作后备

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

代码语言:javascript
复制
self.onfetch = (fetchEvent) => {
   fetchEvent.respondWith(
       caches.match(fetchEvent.request)
       .then((response) => response || fetch(fetchEvent.request))
   );
}; 

通用后备

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

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

catch()子句可以扩展为「支持不同类型的后备」

后记

参考资料:JS高级程序设计第四版

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端柒八九 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章概要
  • 1.服务工作线程Service Worker
  • 基础概念
    • ServiceWorkerContainer
      • 创建服务工作线程
        • 使用 ServiceWorkerContainer 对象
          • 使用 ServiceWorkerRegistration 对象
            • 安全限制
              • 作用域限制
                • 通过「根目录」获取服务脚本对应的「默认根作用域」:
                • 通过「根目录」获取服务脚本但「指定同一目录」作用域
                • 通过「根目录」获取服务脚本但限定了「目录作用域」
                • 通过「嵌套的二级目录」获取服务脚本对应的「同一目录作用域」
            • 线程缓存
              • CacheStorage 对象
                • Cache 对象
                  • options 对象
                • 最大存储空间
                • 线程客户端
                • 生命周期
                  • 已解析状态
                    • 安装中状态
                      • 已安装状态
                        • 激活中状态
                          • 已激活状态
                            • 已失效状态
                              • 使用 serviceWorker.controller 属性
                              • 线程率先发送消息
                          • 控制反转与线程持久化
                          • updateViaCache 管理服务文件缓存
                          • 线程消息
                          • 拦截 fetch 事件
                            • 从网络返回
                              • 从缓存返回
                                • 从网络返回,缓存作后备
                                  • 从缓存返回,网络作后备
                                    • 通用后备
                                    • 后记
                                    相关产品与服务
                                    云服务器
                                    云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档