写点什么

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

发布于: 4 小时前

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

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

"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 事件了。

//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 的工作。

//service worker安装成功后开始缓存所需的资源self.addEventListener('install', function(event) {    //跳过等待过程    self.skipWaiting();});
复制代码

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

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

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 最为关键的一个阶段,用于拦截代理所有指定的请求,并进行对应的操作。

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

//监听浏览器的所有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生命周期


Service Worker 信息通讯

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

从页面到 Service Worker

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

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

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 。

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 ,不过这种方法只能向消息的来源页面发送信息。

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

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

// sw.jsthis.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 。

// index.jsnavigator.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.jsthis.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按需依赖


做到按需依赖的原理就是通过 Proxy 对全局对象 workbox 进行代理:

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 主动加载:

/** * 加载前端模块 * @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 进行冻结保护:

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


workbox 用到的模块图奉上:


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

发布于: 4 小时前阅读数: 4
用户头像

还未添加个人签名 2021.06.25 加入

还未添加个人简介

评论

发布
暂无评论
ServiceWorker工作机制与生命周期:资源缓存与协作通信处理