service worker

了解 service work 前先了解一下 PWA,那么什么是 PWA 呢?

渐进式网路应用 PWA

  1. Web 应用和 Native 应用有着各自不同的优势和使用场景
  2. PWA 结合了二者的优势
  3. PWA 会越来越流行
    • Web 应用的资源存储在服务器,Native 的资源存储在本地。所以 Native 会比 Web 应用的加载速度和流畅性方面获得更好地表现;
    • PWA 旨在创造拥有更加流畅的用户体验的 Web 应用,和创建类 Native App 的沉浸式效果,而非浏览器端那样的外观和体验;
    • 在各种网络和数据加载的条件下仍然可用-它可以在网络不稳定或者没有网络的情况下使用。

基本架构

Service Worker 是 PWA 的关键技术,可以支持一些原始应用的功能

  • 友好的弱网和离线体验
  • 定期的后台同步
  • 推送通知

Service workers  本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。

1.友好的弱网和离线体验
这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

web worker

  • JavaScript 是一个单线程的语言;
  • 异步编程通过调度一部分代码在 event loop 中执行,从而让程序流畅地运行;
  • Web Workers 是真正的多线程
  • Service Worker 是 Web Worker 的一个子类

webpack 最常见的应用是耗时的计算,比如可视化操作,绘画,web 游戏,电子表单那种上万的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
let worker = new Worker("http://localhots:5157/list.js", {
type: "module",
});

worker.addEventListener("message", (e) => {
console.log(e.data);
});
</script>

<template>
<button
@click="
() => {
worker.postMessage({
a: 123,
});
}
"
>
发消息给worker
</button>
</template>
1
2
3
4
5
6
7
// list.js
let a = 1 + 1;
self.postMessage(a);
self.addEventListener("message", (e) => {
console.log(e);
self.postMessage("收到");
});

特性

  • 它是一种 Web worker,不能直接访问 DOM
  • 它有自己独立的生命周期,不和特定网页相关联
  • 是一个由事件驱动的 worker,它由源和路径组成
  • 可以使用一些离线存储 API — Cache Storage 和 IndexedDB, 不能访问 localStorage
  • 大量使用了 Promise
  • 只能使用 HTTPS,localhost 也被允许

生命周期

  • Service Worker 的生命周期和网页是相互独立的
  • 在网页的 JS 代码中调用 Service Worker 的注册方法开始安装。在安装阶段可以进行一些缓存工作,缓存失败安装就会失败。如果安装成功,代表了缓存也成功完成了
  • 安装成功后触发 activate 事件,Service Worker 处于激活状态
  • 激活后的 Service Worker 线程可以控制页面、监听事件了,它可以根据情况被中止或者唤起

基本功能和用法

  • install 事件会在注册完成之后触发。install 事件一般是被用来填充你的浏览器的离线缓存能力。

为了达成这个目的,我们使用了 Service Worker 的新的标志性的存储 API — cache — 一个 service worker 上的全局对象,它使我们可以存储网络响应发来的资源,并且根据它们的请求来生成 key。
在安装事件的回调里我们需要完成一些缓存的工作,所以需要 waitUntil() 方法来暂时挂起代码,waitUntil 方法接受一个promise

使用 Cache API 缓存资源

Cache 接口为缓存的 Request / Response 对象对提供存储机制

一个域可以有多个命名 Cache 对象。

你需要在你的脚本 (例如,在 ServiceWorker 中)中处理缓存更新的方式。除非明确地更新缓存,否则缓存将不会被更新;除非删除,否则缓存数据不会过期。

CacheStorage 接口表示 Cache 对象存储。

使用通过全局 caches 属性来访问 CacheStorage,可以在 window、Service Worker 中访问。

window.caches // CacheStorage{}

使用 CacheStorage.open(cacheName) 打开一个 Cache 对象,再使用 Cache 对象的方法去处理缓存。

activate 事件在脚本激活后触发,一般在这里处理旧版本的缓存
fetch 事件监听客户端的请求,包括任何被 service worker 控制的文档和文档内引用的资源。配合 respondWith()  方法,可以劫持 HTTP 响应


  • CacheStorage.open()

  • CacheStorage.keys()

  • CacheStorage.match()

  • Cache.addAll(requests)

  • Cache.add(request)

  • Cache.put(request,response)

  • Cache.match(request)

  • Cache.delete(request)

在 install 阶段缓存资源

  • 打开缓存
  • 缓存文件
  • 确认是否所有的静态资源已缓存
1
2
3
4
5
6
7
8
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log("Opened cache");
return cache.addAll(urlsToCache);
})
);
});

缓存运行时请求

  • event.respondWith() 会决定如何响应 fetch 事件。 caches.match() 查询请求及查找之前创建的缓存中是否有任意缓存结果并返回 promise。
  • 如果有,则返回该缓存数据。
  • 否则,执行 fetch 。
  • 检查返回的状态码是否是 200。同时检查响应类型是否为 basic,即检查请求是否同域。当前场景不缓存第三方资源的请求。
  • 把返回数据添加到缓存中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
self.addEventListener("fetch", function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
//复制请求。请求是一个流且只能被使用一次。因为之前已经通过缓存使用过一次了,所以为了在浏览器中使用fetch,需要复制下该请求。
var fetchRequest = event.request.clone();
//没有找到缓存。所以我们需要执行 fetch 以发起请求并返回请求数据。
return fetch(fetchRequest).then(function (response) {
//检测返回数据是否有效
if (!response || response.status !== 200 || response.type !== "basic") {
return response;
}
//复制返回数据,因为它也是流。因为我们想要浏览器和缓存一样使用返回数据,所以必须复制它。这样就有两个流
var responseToCache = response.clone();
caches,
open(CACHE_NAME).then(function (cache) {
//把请求添加到缓存中以备之后的查询用
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});

更新 Service Worker

当用户访问网络应用的时候,浏览器会在后台重新下载包含 Service Worker 代码的  .js  文件。
如果下载下来的文件和当前的 Service Worker 代码文件不同,浏览器会认为文件发生了改变并且会创建一个新的 Service Worker。

创建新的 Service Worker 的过程将会启动,然后触发  install  事件。然而,这时候,旧的 Service Worker 仍然控制着网络应用的页面意即新的 Service Worker 将会处于  waiting  状态。

一旦关闭网络应用当前打开的页面,旧的 Service Worker 将会被浏览器杀死而新安装的 Service Worker 就可以上位了。这时候将会触发  activate  事件。

这是为了避免在不同选项卡中同时运行不同版本的的网络应用所造成的问题-
一些在网页中实际存在的问题且有可能会产生新的 bug(比如当在浏览器中本地存储数据的时候却拥有不同的数据库结构)。

在 activate 阶段清理旧版本的缓存

出现在 activate 回调中的一个常见任务是缓存管理。

这么做的原因是,如果在安装步骤中清除了任何旧缓存,则继续控制所有当前页面的任何旧 Service Worker 将突然无法从缓存中提供文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//清除了旧版本的缓存
self.addEventListener("activate", function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
if (!expectedCaches.includes(cacheNames)) {
return caches.delete(key);
}
})
);
})
);
});

如何在浏览器里面进行调试

Update on reload

  • 重新提取 Service Worker。
  • 即使字节完全相同,也将其作为新版本安装,这表示运行  install  事件并更新缓存。
  • 跳过等待阶段,以激活新 Service Worker。
  • 浏览页面。这意味着每次浏览时(包括刷新)都将进行更新,无需重新加载两次或关闭标签。

Offline 可以离线应用

Bypass for network 可以绕过 Service Worker 的控制

Disable cache 选项不会影响 Service Worker 的缓存;
⌘ + ⇧ + R 强制刷新,会跳过 Service Worker 的控制;

打开 chrome://inspect/#service-workers 管理浏览器中的 Service Worker

参考资料:
ChangbaFE/presentation