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

简介: {服务工作线程|Service Worker}基础概念 ⭐️⭐️⭐️线程缓存 ⭐️⭐️⭐️⭐️线程客户端生命周期 ⭐️⭐️⭐️控制反转与线程持久化updateViaCache 管理服务文件缓存 ⭐️⭐️⭐️线程消息 ⭐️⭐️⭐️拦截 fetch 事件 ⭐️⭐️⭐️⭐️⭐️


虽万物今是而昨非,但昨日之我亦是今日之我  --《心理学通识》

大家好,我是柒八九

前天在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 属性中。

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

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()
    使用接收的 urloptions 对象创建或更新 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()
    显示通知,可以配置 titleoptions 参数。
  • update()
    直接从服务器重新请求服务脚本,如果新脚本不同,则重新初始化。
  • unregister()
    取消服务工作线程的注册。该方法会在服务工作线程执行完再取消注册

安全限制

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

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


作用域限制

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

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

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

wl.jshttps://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 类似于异步 MapCacheStorage 的接口通过全局对象的 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 对象。

服务工作线程缓存只考虑缓存 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。

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 可能的值是 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 事件。

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 已激活');
     }
}); 
复制代码

更可靠的确定服务工作线程处于已激活状态一种方式是检查 ServiceWorkerRegistrationcontroller 属性。该属性会返回激活的 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()方法发送的请求,也能拦截对 JavaScriptCSS、图片和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>

相关文章
|
8天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第28天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细分析线程安全的重要性,以及如何在保证线程安全的同时,提高程序的性能。文章将通过实例代码和理论知识的结合,帮助读者深入理解Java并发编程。
|
16天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】 在多核处理器日益普及的今天,并发编程成为了软件开发中不可忽视的重要话题。Java语言提供了丰富的并发工具和机制来帮助开发者构建高效且线程安全的应用程序。本文将探讨Java并发的核心概念,包括线程同步、锁机制、以及如何通过这些工具实现性能优化。我们将透过实例分析,揭示并发编程中的常见问题,并展示如何利用现代Java API来解决这些问题。
|
16天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】在Java并发编程中,线程安全和性能优化是两个关键要素。本文将深入探讨Java并发编程的基本概念、线程安全的实现方法以及性能优化技巧。通过分析同步机制、锁优化、无锁数据结构和并发工具类的使用,我们将了解如何在保证线程安全的前提下,提高程序的性能。
|
16天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】 在Java开发中,正确处理并发问题对于确保应用的稳定性和提高性能至关重要。本文将深入探讨Java并发编程的核心概念——线程安全,以及如何通过各种技术和策略实现它,同时保持甚至提升系统性能。我们将分析并发问题的根源,包括共享资源的竞争条件、死锁以及线程活性问题,并探索解决方案如同步机制、锁优化、无锁数据结构和并发工具类等。文章旨在为开发者提供一个清晰的指南,帮助他们在编写多线程应用时做出明智的决策,确保应用的高效和稳定运行。
|
6天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化移动应用开发的未来:跨平台框架与原生操作系统的融合
【5月更文挑战第29天】在Java开发中,并发编程是一个重要的议题。随着多核处理器的普及,如何充分利用多核资源,提高程序的执行效率,同时保证数据的安全性和一致性,成为开发者必须面对的挑战。本文将从线程安全的基本概念出发,探讨Java中的线程安全问题,并介绍一些常见的解决方案,如同步机制、锁优化等。最后,我们将通过实例分析,展示如何在保证线程安全的前提下,进行性能优化。
|
7天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第29天】本文将深入探讨Java并发编程的核心概念,包括线程安全、同步机制和性能优化。我们将通过实例分析,帮助您理解如何在实际开发中解决并发问题,提高程序的性能和稳定性。
|
8天前
|
SQL 缓存 监控
基于Java的Web应用性能优化技术
基于Java的Web应用性能优化技术
24 3
|
9天前
|
安全 物联网 Java
未来交织:新兴技术的融合与革新深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第27天】 在数字化的浪潮中,创新技术如同星辰般璀璨,引领着时代的前行。本文聚焦于区块链、物联网(IoT)、虚拟现实(VR)等前沿科技,剖析它们的发展脉络,并探讨这些技术的交互融合与实际应用。通过深入分析,我们预见这些技术将如何重塑经济结构、改善人类生活,并引发社会层面的深刻变革。
|
9天前
|
安全 算法 Java
Java并发编程中的线程安全与性能优化
【5月更文挑战第27天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨如何通过使用synchronized关键字、volatile关键字、Lock接口和原子类等技术来实现线程安全,并介绍如何利用Java并发工具类库来提高程序的性能。
|
9天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第27天】本文深入探讨了Java并发编程的核心概念,包括线程安全和性能优化。我们将详细分析synchronized关键字、volatile变量、原子类和锁等关键技术,并通过实例演示如何在保证线程安全的同时提高程序性能。通过阅读本文,您将能够更好地理解Java并发编程的原理,并在实际开发中应用这些知识。