人生就像钟摆,晃动在痛苦和无聊之间,其动力便是欲望
大家好,我是柒八九。
前言
在前几天师夷长技以制夷:跟着PS学前端技术文件中,我们提到了WorkBox
,然后自己也对这块很感兴趣,所以就利用业余时间进行相关资源的查询学习和实践。在学习过程中发现,想要弄明白WorkBox
,有一点很关键,我们需要搞懂Service Worker
。
而在之前的
其实已经写过相关的文章,但是由于当时的技术所限,其中的内容只是单纯的从实现逻辑上,也就是API
层面做了一次不完整归纳总结。总体从Worker
层面的继承关系和简单使用方面出发。
而,今天我们再次对Service Worker
做一次深度的剖析。当然,其中API
的部分大家可以翻看之前的文章。下文中不再赘述。
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 前置知识点
- service workers 能为我们带来什么
- Service worker 的生命周期
- Service worker 缓存策略
- Service Worker 预缓存的陷阱
- 改进Service Worker开发体验
1. 前置知识点
前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略
同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用。
如何查看Service Worker
要查看正在运行的Service workers
列表,我们可以在Chrome/Chromium
中地址栏中输入chrome://serviceworker-internals/
。
chrome://xx
包含了很多内置的功能,这块也是有很大的说道的。后期,会单独有一个专题来讲。(已经在筹划准备中....)
Cache API
Cache API
为缓存的Request
/Response
对象对提供存储机制。例如,作为ServiceWorker
生命周期的一部分
Cache API
像 workers
一样,是暴露在 window
作用域下的。尽管它被定义在 service worker
的标准中,但是它不必一定要配合 service worker
使用。
一个域可以有多个命名 Cache 对象。我们需要在脚本 (例如,在 ServiceWorker
中) 中处理缓存更新的方式。
- 除非明确地更新缓存,否则缓存将不会被更新;
- 除非删除,否则缓存数据不会过期
- 使用
CacheStorage.open(cacheName)
打开一个Cache 对象
,再使用Cache 对象
的方法去处理缓存。 - 需要定期地清理缓存条目,因为每个浏览器都硬性限制了一个域下缓存数据的大小。
- 缓存配额使用估算值,可以使用
StorageEstimate API
获得。 - 浏览器尽其所能去管理磁盘空间,但它有可能删除一个域下的缓存数据。
- 浏览器要么自动删除特定域的全部缓存,要么全部保留。
一些围绕service worker
缓存的重要 API 方法包括:
CacheStorage.open
用于创建新的 Cache 实例。Cache.add
和Cache.put
用于将网络响应存储在service worker
缓存中。Cache.match
用于查找 Cache 实例中的缓存响应。Cache.delete
用于从 Cache 实例中删除缓存响应。- .....
Cache.put
,Cache.add
和Cache.addAll
只能在GET请求
下使用。
更多详情可以参考MDN-Cache
Cache API 与 HTTP 缓存的区别
如果我们以前没有使用过Cache接口
,可能会认为它与 HTTP 缓存
相同,或者至少与 HTTP 缓存
相关。但实际情况并非如此。
Cache接口
是一个完全独立于HTTP 缓存
的缓存机制- 用于影响
HTTP缓存
的任何Cache-Control
配置对存储在Cache接口
中的资源没有影响。
可以将浏览器缓存看作是分层的。
HTTP缓存
是一个由键-值对驱动的低级缓存,其中的指令在HTTP Header
中表示。Cache接口
是由JavaScript API 驱动的高级缓存。这比使用相对简单的HTTP键-值对
具有更大的灵活性。
2. service workers 能为我们带来什么
Service workers
是JavaScript
层面的 API,充当 Web 浏览器和 Web 服务器之间的代理。它们的目标是通过提供离线访问以及提升页面性能来提高可靠性。
渐进增强,类似应用程序生命周期
Service workers
是对现有网站的增强。这意味着如果使用Service workers
的网站的用户使用不支持Service workers
的浏览器访问网站,基本功能不会受到破坏。它是向下兼容的。
Service workers
通过类似于桌面应用程序的生命周期逐渐增强网站。想象一下当从应用商城安装APP
时会发生流程:
- 发出下载
APP
的请求。 APP
下载并安装。APP
准备好使用并可以启动。APP
进行新版本的更新。
Service worker
也采用类似的生命周期,但采用渐进增强的方法。
- 在首次访问安装了新
Service worker
的网页时,初始访问提供网站的基本功能,同时Service worker
开始下载。 - 安装和激活
Service worker
后,它将控制页面以提供更高的可靠性
和速度
。
采用 JavaScript 驱动的 Cache API
Service worker
技术中不可或缺的一部分是Cache API
,这是一种完全独立于 HTTP 缓存的缓存机制。Cache API
可以在Service worker
作用域内和主线程作用域内访问。该特性为用户操作与 Cache 实例的交互提供了许多可能性。
HTTP缓存
是通过HTTP Header
中指定的缓存指令来影响的Cache API
可以通过 JavaScript 进行编程
这意味着可以根据网站的特有的逻辑来缓存网络请求的响应
。例如:
- 在首次请求静态资源时将其存储在缓存中,然后在后续请求中从缓存中获取。
- 将
页面结构
存储在缓存中,但在离线情况下从缓存中获取。 - 对于一些非紧急的资源,先从缓存中获取,然后在后台中通过网络再更新它。下次再获取该资源时候,就认为是最新的
- 网络采用流式传输处理部分内容,并与缓存中的应用程序拦截层组合以改善感知性能。
这些都是缓存策略的应用方向。缓存策略使离线体验成为可能,并通过绕过 HTTP 缓存触发的高延迟重新验证检查提供更好的性能。
异步和事件驱动的 API
在网络上传输数据本质上是异步的。请求资产、服务器响应请求以及下载响应都需要时间。所涉及的时间是多样且不确定的。Service workers
通过事件驱动的 API 来适应这种异步性,使用回调处理事件,例如:
- 当
Service worker
正在安装时。 - 当
Service worker
正在激活时。 - 当
Service worker
检测到网络请求时。
都可以使用addEventListener
API 注册事件。所有这些事件都可以与Cache API
进行交互。特别是在网络请求是离散的,运行回调的能力对于提供所期望的可靠性和速度至关重要。
在JavaScript
中进行异步工作涉及使用Promises
。因为Promises
也支持async
和await
,这些JavaScript
特性也可用于简化Service worker
代码,从而提供更好的开发者体验。
预缓存和运行时缓存
Service worker
与Cache实例
之间的交互涉及两个不同的缓存概念:
- 预缓存(
Precaching caching
) - 运行时缓存(
Runtime caching
)
预缓存
是需要提前缓存资源的过程,通常在Service worker
安装期间进行。通过预缓存
,关键的静态资产和离线访问所需的材料可以被下载并存储在 Cache 实例中。这种类型的缓存还可以提高需要预缓存资源的后续页面的页面速度。
运行时缓存
是指在运行时从网络请求资源时应用缓存策略。这种类型的缓存非常有用,因为它保证了用户已经访问过的页面和资源的离线访问。
当在Service worker
中使用这些方法时,可以为用户体验提供巨大的好处,并为普通的网页提供类似应用程序的行为。
与主线程隔离
Service workers
与Web workers
类似,它们的所有工作都在自己的线程上进行。这意味着Service workers
的任务不会与主线程上的其他任务竞争。
我们就以Web Worker
为例子,做一个简单的演示 在JavaScript中创建Web Worker并不是一项复杂的任务。
- 创建一个新的
JavaScript
文件,其中包含我们希望在工作线程
中运行的代码。此文件不应包含对DOM的任何引用,因为它将无法访问DOM。 - 在我们的
主JavaScript
文件中,使用Worker构造函数
创建一个新的Worker对象
。此构造函数接受一个参数,即我们在第1步中创建的JavaScript
文件的URL
。
const worker = new Worker('worker.js');
- 为
Worker对象
添加事件侦听器,以处理主线程和工作线程之间发送的消息。onmessage事件
处理程序用于处理从工作线程发送的消息,而postMessage方法
用于向工作线程发送消息。
worker.onmessage = function(event) { console.log('Worker said: ' + event.data); }; worker.postMessage('Hello, worker!');
- 在我们的工作线程
JavaScript
文件中,添加一个事件侦听器,以处理从主线程发送的消息,使用self对象
的onmessage属性
。我们可以使用event.data
属性访问消息中发送的数据。
self.onmessage = function(event) { console.log('Main thread said: ' + event.data); self.postMessage('Hello, main thread!'); };
现在让我们运行Web应用程序并测试Worker。我们应该在控制台中看到打印的消息,指示主线程和工作线程之间已发送和接收消息。
3. Service worker 的生命周期
定义术语
在深入了解service worker
的生命周期之前,我们先来了解一下与生命周期运作相关的术语(黑话
)
控制和作用域
了解service worker
运作方式的关键在于理解控制(control
)。
- 由
service worker
控制的页面允许service worker
代表该页面进行拦截网络请求。 - 在给定的作用域(
scope
)内,service worker
能够为页面执行处理资源的相关工作。
作用域
一个service worker
的作用域
由其在 Web 服务器上的位置确定。如果一个service worker
在位于/A/index.html
的页面上运行,并且位于/A/sw.js
上,那么该service worker
的作用域
就是/A/
。
- 打开
https://service-worker-scope-viewer.glitch.me/subdir/index.html
。将显示一条消息,说明没有service worker
正在控制该页面。但是,该页面从https://service-worker-scope-viewer.glitch.me/subdir/sw.js
注册了一个service worker
。 - 重新加载页面。因为
service worker
已经注册并处于活动状态,它正在控制页面。将显示一个包含service worker
作用域、当前状态和其 URL 的表单。 - 现在打开
https://service-worker-scope-viewer.glitch.me/index.html
。尽管在此origin
上注册了一个service worker
,但仍然会显示一条消息,说明没有当前的service worker
。这是因为此页面不在已注册service worker
的作用域内。
作用域
限制了service worker
控制的页面。在上面的例子中,这意味着从/subdir/sw.js
加载的service worker
只能控制位于/subdir/
或其子页面中。
控制页面的
service worker
仍然可以拦截任何网络请求,包括跨域资源的请求。作用域
限制了由service worker
控制的页面。
上述是默认情况下作用域
工作的方式,但可以通过设置Service-Worker-Allowed
响应头,以及通过向register
方法传递作用域选项
来进行覆盖。
除非有很好的理由将service worker
的作用域限制为origin
的子集,否则应从 Web 服务器的根目录加载service worker
,以便其作用域尽可能广泛,不必担心Service-Worker-Allowed
头部。
客户端
当说一个service worker
正在控制
一个页面时,实际上是在控制一个客户端。客户端是指URL
位于该service worker
作用域内的任何打开的页面。具体来说,这些是WindowClient
的实例。
3.1 Service worker 在初始化时的生命周期
为了使service worker
能够控制
页面,首先必须将其部署。
让我们看看一个没有service worker
的网站到部署全新service worker
时,中间发生了啥?
1. 注册(Registration
)
注册
是service worker
生命周期的初始步骤:
<script> // 直到页面完全加载后再注册service worker window.addEventListener("load", () => { // 检查service worker是否可用 if ("serviceWorker" in navigator) { navigator.serviceWorker .register("/sw.js") .then(() => { console.log("Service worker 注册成功!"); }) .catch((error) => { console.warn("注册service worker时发生错误:"); console.warn(error); }); } }); </script>
此代码在主线程上运行,并执行以下操作:
- 因为用户首次访问网站时没有注册
service worker
,所以等待页面完全加载后再注册一个。这样可以避免在service worker
预缓存任何内容时出现带宽争用。 - 尽管
service worker
得到了广泛支持,但进行特性检查可以避免在不支持它的浏览器中出现错误。 - 当页面完全加载后,如果支持
service worker
,则注册/sw.js
。
还有一些关键要点:
Service worker
仅在HTTPS
或localhost
上可用。- 如果
service worker
的内容包含语法错误,注册会失败,并丢弃service worker
。 service worker
在一个作用域
内运行。在这里,作用域
是整个origin
,因为它是从根目录加载的。- 当注册开始时,
service worker
的状态被设置为installing
。
一旦注册完成,安装就开始了。
2. 安装(Installation
)
service worker
在注册后触发其install
事件。install
只会在每个service worker
中调用一次,直到它被更新才会再次触发。可以使用addEventListener
在worker
的作用域
内注册install
事件的回调:
// /sw.js self.addEventListener("install", (event) => { const cacheKey = "前端柒八九_v1"; event.waitUntil( caches.open(cacheKey).then((cache) => { // 将数组中的所有资产添加到'前端柒八九_v1'的`Cache`实例中以供以后使用。 return cache.addAll([ "/css/global.bc7b80b7.css", "/css/home.fe5d0b23.css", "/js/home.d3cc4ba4.js", "/js/A.43ca4933.js", ]); }) ); });
这会创建一个新的Cache实例
并对资产进行预缓存。其中有一个event.waitUntil
。event.waitUntil
接受一个Promise
,并等待该Promise
被解决。
在这个示例中,这个Promise
执行两个异步操作:
- 创建一个名为
前端柒八九_v1
的新Cache实例
。 - 在创建缓存之后,使用其异步的
addAll
方法预缓存一个资源URL数组
。
如果传递给event.waitUntil
的Promise
被拒绝,安装将失败。如果发生这种情况,service worker
将被丢弃。
如果Promise
被解决,安装成功,service worker
的状态将更改为installed
,然后进入激活阶段。