前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ServiceWorker工作机制与生命周期:资源缓存与协作通信处理

ServiceWorker工作机制与生命周期:资源缓存与协作通信处理

原创
作者头像
周陆军
发布2021-07-03 16:53:17
1.3K0
发布2021-07-03 16:53:17
举报
文章被收录于专栏:前端架构前端架构

在 《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 的实例

参考网易新闻的注册方式:

代码语言:javascript
复制
"serviceWorker"?in?navigator?&&?window.addEventListener("load",()=>{
??var?e?=?location.pathname.match(/\/news\/[a-z]{1,}\//)[0]?+?"article-sw.js?v=hash";
??navigator.serviceWorker.register(e).then((n)=>?{
????n.onupdatefound?=?function()?{
??????var?e?=?n.installing;
??????e.onstatechange?=?function()?{
????????switch?(e.state)?{
??????????case?"installed":
????????????navigator.serviceWorker.controller???console.log("New?or?updated?content?is?available.")?:?console.log("Content?is?now?available?offline!");
????????????break;
??????????case?"redundant":
????????????console.error("The?installing?service?worker?became?redundant.")
????????}
??????}
????}
??}).
??catch(function(e)?{
????console.error("Error?during?service?worker?registration:",?e)
??})
})

前面提到过,由于sw会监听和代理所有的请求,所以sw的作用域就显得额外的重要了,比如说我们只想监听我们专题页的所有请求,就在注册时指定路径:navigator.serviceWorker.register('/topics/sw.js');这样就只会对topics/下面的路径进行优化。

installing

注册完 Service Worker 之后,浏览器会为我们自动安装它,因此我们就可以在 service worker 文件中监听它的 install 事件了。

代码语言:javascript
复制
//service?worker安装成功后开始缓存所需的资源
var?CACHE_PREFIX?=?'cms-sw-cache';
var?CACHE_VERSION?=?'0.0.20';
var?CACHE_NAME?=?CACHE_PREFIX+'-'+CACHE_VERSION;
var?allAssets?=?[
????'./main.css'
];
self.addEventListener('install',?(event)=>?{
????//调试时跳过等待过程
????self.skipWaiting();

????//?Perform?install?steps
????//首先?event.waitUntil?你可以理解为?new?Promise,
????//它接受的实际参数只能是一个?promise,因为,caches?和?cache.addAll?返回的都是?Promise,
????//这里就是一个串行的异步加载,当所有加载都成功时,那么?SW?就可以下一步。
????//另外,event.waitUntil?还有另外一个重要好处,它可以用来延长一个事件作用的时间,
????//这里特别针对于我们?SW?来说,比如我们使用?caches.open?是用来打开指定的缓存,但开启的时候,
????//并不是一下就能调用成功,也有可能有一定延迟,由于系统会随时睡眠?SW,所以,为了防止执行中断,
????//就需要使用?event.waitUntil?进行捕获。另外,event.waitUntil?会监听所有的异步?promise
????//如果其中一个?promise?是?reject?状态,那么该次?event?是失败的。这就导致,我们的?SW?开启失败。
????event.waitUntil(
??????caches.open(CACHE_NAME)
????????.then(function(cache)?{
??????????console.log('[SW]:?Opened?cache');
??????????return?cache.addAll(allAssets);
????????})
????);
}

安装时,sw就开始缓存文件了,会检查所有文件的缓存状态,如果都已经缓存了,则安装成功,进入下一阶段。

activated

同样的,Service Worker 在安装完成后会被激活,所以我们也可监听 activate 事件。这时,我们可以在 Chorme 的开发者工具中看到我们注册的 Service Worker。

如果是第一次加载sw,在安装后,会直接进入activated阶段,而如果sw进行更新,情况就会显得复杂一些。流程如下:

  • 首先老的sw为A,新的sw版本为B。
  • B进入install阶段,而A还处于工作状态,所以B进入waiting阶段。只有等到A被terminated后,B才能正常替换A的工作。
代码语言:javascript
复制
//service?worker安装成功后开始缓存所需的资源
self.addEventListener('install',?function(event)?{
????//跳过等待过程
????self.skipWaiting();
});

然后就进入了activated阶段,激活sw工作。

activated阶段可以做很多有意义的事情,比如更新存储在cache中的key和value:

代码语言:javascript
复制
var?CACHE_PREFIX?=?'cms-sw-cache';
var?CACHE_VERSION?=?'0.0.20';
/**
?*?找出对应的其他key并进行删除操作
?*?@returns?{*}
?*/
function?deleteOldCaches()?{
????return?caches.keys().then(function?(keys)?{
????????var?all?=?keys.map(function?(key)?{
????????????if?(key.indexOf(CACHE_PREFIX)?!==?-1?&&?key.indexOf(CACHE_VERSION)?===?-1){
????????????????console.log('[SW]:?Delete?cache:'?+?key);
????????????????return?caches.delete(key);
????????????}
????????});
????????return?Promise.all(all);
????});
}
//sw激活阶段,说明上一sw已失效
self.addEventListener('activate',?function(event)?{
????event.waitUntil(
????????//?遍历?caches?里所有缓存的?keys?值
????????caches.keys().then(deleteOldCaches)
????);
});

idle

这个空闲状态一般是不可见的,这种一般说明sw的事情都处理完毕了,然后处于闲置状态了。

浏览器会周期性的轮询,去释放处于idle的sw占用的资源。

fetch

该阶段是sw最为关键的一个阶段,用于拦截代理所有指定的请求,并进行对应的操作。

所有的缓存部分,都是在该阶段,这里举一个简单的例子:

代码语言:javascript
复制
//监听浏览器的所有fetch请求,对已经缓存的资源使用本地缓存回复
self.addEventListener('fetch',?function(event)?{
????event.respondWith(
????????caches.match(event.request)
????????????.then(function(response)?{
????????????????//该fetch请求已经缓存
????????????????if?(response)?{
????????????????????return?response;
????????????????}
????????????????return?fetch(event.request);
????????????????}
????????????)
????);
});

下面放出service 生命周期图

Service Worker信息通讯

postMessage 方法可以进行 Service Worker 和页面之间的通讯

从页面到 Service Worker

navigator.serviceWorker.controller.postMessage("this message is from page");

为了保证 Service Worker 能够接收到信息,必须被注册完成之后再发送信息

代码语言:javascript
复制
navigator.serviceWorker.register('./sw.js',?{scope:?'./sw'})
??.then(function?(reg)?{
????console.log('success',?reg);
????reg.active.postMessage("this?message?is?from?page,?to?sw");
??});

请注意,当我们在注册 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 。

代码语言:javascript
复制
navigator.serviceWorker.register('./sw/sw.js')
??.then(function?(reg)?{
????return?new?Promise((resolve,?reject)?=>?{
??????const?interval?=?setInterval(function?()?{
????????if?(reg.active)?{
??????????clearInterval(interval);
??????????resolve(reg.active);
????????}
??????},?100);
????});
??}).then(sw?=>?{
??sw.postMessage("this?message?is?from?page,?to?sw");

这个感觉有点坑

从 Service Worker 到页面

?Service Worker 发送信息到页面了,不同于页面向 Service Worker 发送信息,我们需要在 WindowClient 实例上调用 postMessage 方法才能达到目的。而在页面的JS文件中,监听 navigator.serviceWorker 的 message 事件即可收到信息。

而最简单的方法就是从页面发送过来的消息中获取 WindowClient 实例,使用的是 event.source ,不过这种方法只能向消息的来源页面发送信息。

代码语言:javascript
复制
//?sw.js
this.addEventListener('message',?function?(event)?{
??event.source.postMessage('this?message?is?from?sw.js,?to?page');
});
?
//?index.js
navigator.serviceWorker.addEventListener('message',?function?(e)?{
??console.log(e.data);?//?this?message?is?from?sw.js,?to?page
});

如果不想受到这个限制,则可以在 serivce worker 文件中使用 this.clients 来获取其他的页面,并发送消息。

代码语言:javascript
复制
//?sw.js
this.clients.matchAll().then(client?=>?{
??client[0].postMessage('this?message?is?from?sw.js,?to?page');
})

如果在注册 Service Worker 的时候,把 scope 设置为非 origin 目录,那么在 service worker 文件中,我无法获取到 Origin 路径对应页面的 client。

使用Message Channel 来通信

比较好用的通信方式是使用 Message Channel 。

代码语言:javascript
复制
//?index.js
navigator.serviceWorker.register('./sw.js',?{?scope:?'./'?})
????.then(function?(reg)?{
??????const?messageChannel?=?new?MessageChannel();
??????messageChannel.port1.onmessage?=?e?=>?{
????????console.log(e.data);?//?this?message?is?from?sw.js,?to?page
??????}
??????reg.active.postMessage("this?message?is?from?page,?to?sw",?[messageChannel.por2]);
????})
?
//?sw.js
this.addEventListener('message',?function?(event)?{
??console.log(event.data);?//?this?message?is?from?page,?to?sw
??event.ports[0].postMessage('this?message?is?from?sw.js,?to?page');
});

使用这种方式能够使得通道两端之间可以相互通信,而不是只能向消息源发送信息。

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进行代理:

代码语言:javascript
复制
new?Proxy(this,?{
??get(t,?s)?{
????//如果workbox对象上不存在指定对象,就依赖注入该对象对应的脚本
????if?(t[s])?return?t[s];
????const?o?=?e[s];
????return?o?&&?t.loadModule(`workbox-${o}`),?t[s];
??}
})

如果找不到对应模块,则通过importScripts主动加载:

代码语言:javascript
复制
/**
?*?加载前端模块
?*?@param?{Strnig}?t?
?*/
loadModule(t)?{
??const?e?=?this.o(t);
??try?{
????importScripts(e),?(this.s?=?!0);
??}?catch?(s)?{
????throw?(console.error(`Unable?to?import?module?'${t}'?from?'${e}'.`),?s);
??}
}

具体看下源码更好

通过freeze冻结对外暴露api

workbox.core模块中提供了几个核心操作模块,如封装了indexedDB操作的DBWrapper、对cacheStorage进行读取的cacheWrapper,以及发送请求的fetchWrapper和日志管理的logger等等。

为了防止外部对内部模块暴露出去的api进行修改,导致出现不可预估的错误,内部模块可以通过Object.freeze将api进行冻结保护:

代码语言:javascript
复制
?var?_private?=?/*#__PURE__*/Object.freeze({
????DBWrapper:?DBWrapper,
????WorkboxError:?WorkboxError,
????assert:?finalAssertExports,
????cacheNames:?cacheNames,
????cacheWrapper:?cacheWrapper,
????fetchWrapper:?fetchWrapper,
????getFriendlyURL:?getFriendlyURL,
????logger:?defaultExport
??});

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前端缓存分析
    • storage
      • 前端数据库:
        • manifest 缓存
        • Service Worker的使用
          • register
            • installing
              • activated
                • idle
                  • fetch
                  • Service Worker信息通讯
                    • 从页面到 Service Worker
                      • 从 Service Worker 到页面
                        • 使用Message Channel 来通信
                        • Workbox
                          • workbox缓存策略
                            • staleWhileRevalidate
                            • networkFirst
                            • cacheFirst
                            • networkOnly
                            • cacheOnly
                          • workbox原理
                            • 通过Proxy按需依赖
                            • 通过freeze冻结对外暴露api
                        相关产品与服务
                        云开发 CLI 工具
                        云开发 CLI 工具(Cloudbase CLI Devtools,CCLID)是云开发官方指定的 CLI 工具,可以帮助开发者快速构建 Serverless 应用。CLI 工具提供能力包括文件储存的管理、云函数的部署、模板项目的创建、HTTP Service、静态网站托管等,您可以专注于编码,无需在平台中切换各类配置。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                        http://www.vxiaotou.com