ServiceWorker 工作机制与生命周期:资源缓存与协作通信处理
在 《web messaging与Woker分类:漫谈postMessage跨线程跨页面通信》介绍过 ServiceWorker,这里摘抄跟多的内容,补全
Service Worker 理解为一个介于客户端和服务器之间的一个代理服务器。在 Service Worker 中我们可以做很多事情,比如拦截客户端的请求、向客户端发送消息、向服务器发起请求等等,其中最重要的作用之一就是离线资源缓存。
前端缓存分析
前端缓存 大致可以分为 http 缓存 与 浏览器缓存
http 缓存推荐阅读《浏览器http缓存机制剖析:存储策略与过期策略的机理分析》,我们来分析下 浏览器缓存
storage
cookie、localStorage、sessionStorage
cookie 最大约为 4k,每个域名最多 50kcookie——不同浏览器限制不一样,一般用来存储关键数据(比如用户登录信息)
localStorage/sessionStorage 通常有 5MB 的存储空间,比如微信文章 不需要改动的资源(如 css/js)就基本存储在 localStorage 里面
推荐阅读《登录状态控制:cookies对比sessionStorage保持信息的分析》
前端数据库:
WebSql 和 IndexDB,其中 WebSql 被规范废弃,他们都有大约 50MB 的最大容量,一般 当页面 store 的数据可以直接存储在里面。
manifest 缓存
已经被废弃,因为他的设计有些不合理的地方,他在缓存静态文件的同时,也会默认缓存 html 文件。这导致页面的更新只能通过 manifest 文件中的版本号来决定。所以,应用缓存只适合那种常年不变化的静态网站。如此的不方便,也是被废弃的重要原因。
推荐阅读《html5离线缓存manifest详解》、《HTML5离线存储实战之manifest的那些坑》
Service Worker 本质上也是浏览器缓存资源用的,只不过他不仅仅是 cache,也是通过 worker 的方式来进一步优化。Service Worker
他基于 h5 的 web worker,所以绝对不会阻碍当前 js 线程的执行,sw 最重要的工作原理就是
后台线程:独立于当前网页线程;
网络代理:在网页发起请求时代理,来缓存文件——因为 service worker 中涉及到请求拦截,出于对安全问题的考虑,所以必须使用 HTTPS 协议来保障安全
被缓存的文件可在 Network 中看到 Size 项为 from ServiceWorker,在 Application 的 Cache Storage 可查看缓存的具体内容(本地 localhost 调试)
如果是具体的断点调试,需要使用对应的线程,不再是 main 线程了,这也是 webworker 的通用调试方法:
Service Workers 一般作为 web 应用程序、浏览器和网络(如果可用)之间的代理服务。他们旨在(除开其他方面)创建有效的离线体验,拦截网络请求,以及根据网络是否可用采取合适的行动,更新驻留在服务器上的资源。他们还将允许访问推送通知和后台同步 API。
Service worker 运行在 worker 上下文,因此它不能访问 DOM。相对于驱动应用的主 JavaScript 线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步 API(如 XHR 和 localStorage)不能在 service worker 中使用。
不同于普通 Worker,Service Worker 是一个浏览器中的进程而不是浏览器内核下的线程(Service Worker 是走的另外的线程,可以理解为在浏览器背后默默运行的一个线程,或者说是独立于当前页面的一段运行在浏览器后台进程里的脚本。)因此它在被注册安装之后,能够被在多个页面中使用,也不会因为页面的关闭而被销毁。
出于对安全问题的考虑,Service Worker 只能被使用在 https 或者本地的 localhost 环境下。
使用 ServiceWorkerContainer.register() 方法首次注册 service worker。如果注册成功,service worker 就会被下载到客户端并尝试安装或激活(见下文),这将作用于整个域内用户可访问的 URL,或者其特定子集。
Service Worker 的使用
Service worker 是一个注册在指定源和路径下的事件驱动 worker。它采用 JavaScript 控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。
register
要使用 Service worker,首先需要注册一个 sw,通知浏览器为该页面分配一块内存,然后 sw 就会进入安装阶段。
navigator.serviceWorker.register(path,object)
path: service worker 文件的路径,请注意:这个文件路径是相对于 Origin ,而不是当前 JS 文件的目录的
object: Serivce Worker 的配置项,可选填,其中比较重要的是 scope 属性,它是 Service Worker 控制的内容的子目录
Service Worker 的 register 方法返回的是一个 Promise 。如果注册失败,可以通过 catch 来捕获错误信息;如果注册成功,可以使用 then 来获取一个 ServiceWorkerRegistration 的实例
参考网易新闻的注册方式:
前面提到过,由于 sw 会监听和代理所有的请求,所以 sw 的作用域就显得额外的重要了,比如说我们只想监听我们专题页的所有请求,就在注册时指定路径:navigator.serviceWorker.register('/topics/sw.js');这样就只会对 topics/下面的路径进行优化。
installing
注册完 Service Worker 之后,浏览器会为我们自动安装它,因此我们就可以在 service worker 文件中监听它的 install 事件了。
安装时,sw 就开始缓存文件了,会检查所有文件的缓存状态,如果都已经缓存了,则安装成功,进入下一阶段。
activated
同样的,Service Worker 在安装完成后会被激活,所以我们也可监听 activate 事件。这时,我们可以在 Chorme 的开发者工具中看到我们注册的 Service Worker。
如果是第一次加载 sw,在安装后,会直接进入 activated 阶段,而如果 sw 进行更新,情况就会显得复杂一些。流程如下:
首先老的 sw 为 A,新的 sw 版本为 B。
B 进入 install 阶段,而 A 还处于工作状态,所以 B 进入 waiting 阶段。只有等到 A 被 terminated 后,B 才能正常替换 A 的工作。
然后就进入了 activated 阶段,激活 sw 工作。
activated 阶段可以做很多有意义的事情,比如更新存储在 cache 中的 key 和 value:
idle
这个空闲状态一般是不可见的,这种一般说明 sw 的事情都处理完毕了,然后处于闲置状态了。
浏览器会周期性的轮询,去释放处于 idle 的 sw 占用的资源。
fetch
该阶段是 sw 最为关键的一个阶段,用于拦截代理所有指定的请求,并进行对应的操作。
所有的缓存部分,都是在该阶段,这里举一个简单的例子:
下面放出 service 生命周期图
Service Worker 信息通讯
postMessage 方法可以进行 Service Worker 和页面之间的通讯
从页面到 Service Worker
navigator.serviceWorker.controller.postMessage("this message is from page");
为了保证 Service Worker 能够接收到信息,必须被注册完成之后再发送信息。
请注意,当我们在注册 Service Worker 时,如果使用的 scope 不是 Origin ,那么 navigator.serviceWorker.controller 会为 null。这种情况下,我们可以使用 reg.active 这个对象下的 postMessage 方法,reg.active 就是被注册后激活 Serivce Worker 实例。但是,由于 Service Worker 的激活是异步的,因此第一次注册 Service Worker 的时候,Service Worker 不会被立刻激活, reg.active 为 null,系统会报错。我采用的方式是返回一个 Promise ,在 Promise 内部进行轮询,如果 Service Worker 已经被激活,则 resolve 。
这个感觉有点坑
从 Service Worker 到页面
Service Worker 发送信息到页面了,不同于页面向 Service Worker 发送信息,我们需要在 WindowClient 实例上调用 postMessage 方法才能达到目的。而在页面的 JS 文件中,监听 navigator.serviceWorker 的 message 事件即可收到信息。
而最简单的方法就是从页面发送过来的消息中获取 WindowClient 实例,使用的是 event.source ,不过这种方法只能向消息的来源页面发送信息。
如果不想受到这个限制,则可以在 serivce worker 文件中使用 this.clients 来获取其他的页面,并发送消息。
如果在注册 Service Worker 的时候,把 scope 设置为非 origin 目录,那么在 service worker 文件中,我无法获取到 Origin 路径对应页面的 client。
使用 Message Channel 来通信
比较好用的通信方式是使用 Message Channel 。
使用这种方式能够使得通道两端之间可以相互通信,而不是只能向消息源发送信息。
Workbox
由于直接写原生的 sw.js,比较繁琐和复杂,所以一些工具就出现了,而 workbox 是其中的佼佼者,由 google 团队推出。
在 Workbox 之前,GoogleChrome 团队较早时间推出过 sw-precache 和 sw-toolbox 库,但是在 GoogleChrome 工程师们看来,workbox 才是真正能方便统一的处理离线能力的更完美的方案,所以停止了对 sw-precache 和 sw-toolbox 的维护。
workbox 缓存策略
workbox.strategies,有如下属性:staleWhileRevalidate networkFirst cacheFirst networkOnly cacheOnly ,通过不同的配置可以针对自己的业务达到不同的效果
staleWhileRevalidate
这种策略的意思是当请求的路由有对应的 Cache 缓存结果就直接返回,
在返回 Cache 缓存结果的同时会在后台发起网络请求拿到请求结果并更新 Cache 缓存,如果本来就没有 Cache 缓存的话,直接就发起网络请求并返回结果,这对用户来说是一种非常安全的策略,能保证用户最快速的拿到请求的结果。
但是也有一定的缺点,就是还是会有网络请求占用了用户的网络带宽。
networkFirst
这种策略就是当请求路由是被匹配的,就采用网络优先的策略,也就是优先尝试拿到网络请求的返回结果,如果拿到网络请求的结果,就将结果返回给客户端并且写入 Cache 缓存。
如果网络请求失败,那最后被缓存的 Cache 缓存结果就会被返回到客户端,这种策略一般适用于返回结果不太固定或对实时性有要求的请求,为网络请求失败进行兜底。可以像如下方式使用 Network First 策略:
cacheFirst
这个策略的意思就是当匹配到请求之后直接从 Cache 缓存中取得结果,如果 Cache 缓存中没有结果,那就会发起网络请求,拿到网络请求结果并将结果更新至 Cache 缓存,并将结果返回给客户端。这种策略比较适合结果不怎么变动且对实时性要求不高的请求。
networkOnly
比较直接的策略,直接强制使用正常的网络请求,并将结果返回给客户端,这种策略比较适合对实时性要求非常高的请求。
cacheOnly
这个策略也比较直接,直接使用 Cache 缓存的结果,并将结果返回给客户端,这种策略比较适合一上线就不会变的静态资源请求。
workbox 原理
通过 Proxy 按需依赖
熟悉了 workbox 后会得知,它是有很多个子模块的,各个子模块再通过用到的时候按需 importScript 到线程中。
做到按需依赖的原理就是通过 Proxy 对全局对象 workbox 进行代理:
如果找不到对应模块,则通过 importScripts 主动加载:
具体看下源码更好
通过 freeze 冻结对外暴露 api
workbox.core 模块中提供了几个核心操作模块,如封装了 indexedDB 操作的 DBWrapper、对 cacheStorage 进行读取的 cacheWrapper,以及发送请求的 fetchWrapper 和日志管理的 logger 等等。
为了防止外部对内部模块暴露出去的 api 进行修改,导致出现不可预估的错误,内部模块可以通过 Object.freeze 将 api 进行冻结保护:
workbox 用到的模块图奉上:
参考文章:
serviceworker 运用与实践 https://blog.csdn.net/mevicky/article/details/86605882
Service Worker —这应该是一个挺全面的整理 https://juejin.im/post/5b06a7b3f265da0dd8567513
转载本站文章《ServiceWorker工作机制与生命周期:资源缓存与协作通信处理》,请注明出处:https://www.zhoulujun.cn/html/webfront/SGML/html5/2020_0617_8470.html
版权声明: 本文为 InfoQ 作者【周陆军的微博】的原创文章。
原文链接:【http://xie.infoq.cn/article/7f764326dd918f3ea28ebbcee】。文章转载请联系作者。
评论