浏览器缓存和Service Worker

转载;https://www.cnblogs.com/bill-shooting/archive/2018/07/21/9347441.htmljavascript

1. 传统的HTTP浏览器缓存策略

在一个网页的生命周期中,开发者为了缩短用户打开页面的时间,一般会设置不少缓存。其中包括了:css

  • 浏览器缓存
  • 代理服务器缓存(CDN缓存)
  • 服务器缓存
  • 数据库缓存

等各类缓存。这些缓存大多数和前端没什么关系,也不禁前端开发者控制,其中和前端较为密切的是浏览器缓存,但它本质上也是由服务器控制的。html

在Service Worker还未问世以前,浏览器缓存主要是由HTTP缓存策略和浏览器内置的存储功能(cookie,Local Storage,Session Storage等)来提供。其中HTTP缓存因为是钦定的,根正苗红,浏览器支持的也很好,是最经常使用的浏览器缓存技术。而经过浏览器内置存储功能来实现缓存,相比之下就没那么高端大气上档次了。由于这种方式没个标准范式,虽然说能够经过JS进行控制显得比HTTP缓存灵活,但效果嘛就只能依赖程序员的水平了,也没有个统一的轮子能用,因此这种方式也就是小打小闹,不成气候。前端

下面介绍一下HTTP缓存的一些用法:java

  • Expires头部
    早在HTTP协议被设计的时候,协议的起草者们就想到了缓存的事情,天然也有相应的功能,那就是Expires这个头部。每当浏览器请求时,服务器能够在相应的报文中附加这个Expires,它的典型值看起来是这样的:
Expires: Tue, 01 May 2018 11:37:06 GMT

也就是在该资源在世界协调时2018/05/01 11:37:06才过时,个人请求时间是2018/05/01 07:37:06,因此就是这个资源在4小时以后过时,4小时以内对该资源的请求都直接使用缓存,除非你用Ctrl+F5刷新。可是呢,这种控制明显是不够精细的,这是个HTTP1.0协议中规定的头部。因为咱们如今都用HTTP2.0都已经来了,HTTP1.1已经全面普及了,这玩意天然已经用的很少了。程序员

  • Cache-Control头部
    Expires头部只能控制过时时间,万一请求的资源在过时时间以前就更新了,那就可能会出现显示或者功能问题。为此,HTTP协议再更新到1.1版本的时候,增长了一个新的头部Cache-Control并规定:若是同时存在Cache-ControlExpires前者有效。它有如下经常使用的值可选:public private max-age s-maxage no-cache no-store等。一个典型的值看起来是如下这样:
Cache-Control: s-maxage=300, public, max-age=60

为了更好的说明各个字段的意义,先说下浏览器请求资源的步骤:数据库

  1. 判断请求是否命中缓存,如命中则执行步骤2;如没有则执行步骤3;
  2. 判断缓存是否过时,如没有则直接返回;如过时则执行步骤3,并带上缓存信息;
  3. 浏览器向服务器请求资源;
  4. 服务器判断缓存信息,如资源还没有更新,则返回304,如没有缓存信息或则资源已更新则返回200,并把资源返回。
  5. 浏览器根据响应头部决定要不要存储缓存(只有no-store时不存储缓存信息)。

s-maxage表示共享缓存的时间,单位是s,也就是5分钟;
public表示这是个共享缓存,能够被其余session使用;
max-age意义与s-maxage差很少,只是它用于private的情形;
no-cache这种策略下,浏览器会跳过步骤2,并带上缓存信息向服务器发起请求。
no-store这种策略下,浏览器会跳过步骤5,因为没有缓存信息,每次浏览器请求时都不会带上缓存信息,就像第一次请求同样(Ctrl+F5效果)。json

  • Last-Modified/If-Modified-Since
    上面说了,浏览器在有缓存信息的状况下,会带上缓存信息发起请求,那这个信息是怎么来的?又是怎么带在Request的头部当中呢?
    原来,服务器在响应请求时,除了返回Cache-Control头部外,还会返回一个Last-Modified头部,用于指定该资源的服务器更新时间。当该资源在浏览器端过时时(由max-age或者no-cache决定),浏览器会带上缓存信息去发起请求,这个信息就由Request中的If-Modified-Since指定,一般也就是上次Response中Last-Modified的值。典型值以下:
//Response: Last-Modified: Sat, 01 Jan 2000 00:00:00 GMT //Request: If-Modified-Since: Sat, 01 Jan 2000 00:00:00 GMT
  • Etag/If-None-Match
    Last-Modified/If-Modified-Since提供的控制已经比较多了,但有些时候,开发者仍是不满意,由于它们只能提供对资源时间的控制,并只有精确到秒级。若是有些资源变化很是快,或者有些资源定时生成,但内容倒是同样的,这些状况下Last-Modified/If-Modified-Since就不是很适用。
    为此,HTTP1.1规定了Etag/If-None-Match这两个头部,它们的用法和Last-Modified/If-Modified-Since彻底相同,一个用于响应,一个用于请求。只不过Etag用的不是时间,而是服务器规定的一个标签(一般是资源内容、大小、时间的hash值)。这样服务器经过这个头部能够更加啊精确地控制资源的缓存策略。
    一样的,因为这个头部控制更加精细, 因此它的优先级会高于Last-Modified/If-Modified-Since,就像Cache-Control高于Expires同样。

2. Service Worker的原理

HTTP缓存已经足够强大了,那开发者还有什么不满意呢?后端的开发者天然没什么不满意,前端的开发者就要嘀咕了:“浏览器的事情,为何要依赖于后端呢?后端就好好提供数据就好了,缓存这种事情我想本身控制”。确实有人这么尝试过,就是以前说的用Local Storage或者Session Storage来存储一些数据,但这种方法缺乏不少关键的浏览器基础设施,好比异步存储、静态资源存储、URL匹配、请求拦截等功能。而Service Worker的出现填补了这些基础设施缺乏的问题。后端

须要指出的是,Service Worker并不是专门为缓存而设计,它还能够解决Web应用推送、后台长计算等问题。能解决精细化缓存控制,实在是因为它的功能强大,由于它本质上就是一个全新的JavaScript线程,运行在与主Javascript线程不一样的上下文。service worker线程被设计成完成异步,一些本来在主线程中的同步API,如XMLHTTPRequestlocalStorage是不能在service worker中使用的。promise

主Javascript线程是负责DOM的线程,而service worker线程被设计成没法访问DOM。这是很天然的,通常从事过客户端开发的开发者都知道,只能有一个UI线程,不然整个UI的控制会出现不可预估的问题。而保证UI顺滑不卡顿的原则就是尽可能不在UI线程作大量计算和同步IO处理

  1. sw线程可以用来和服务器沟通数据(service worker的上下文内置了fetch和Push API)
  2. 可以用来进行大量复杂的运算而不影响UI响应。
  3. 它能拦截全部的请求(经过监听fetch事件,任何对网络资源的请求都会触发该事件),并内置了一个彻底异步的存储系统(Caches属性,彻底异步并能存储所有种类的网络资源),这是它能精细化控制缓存的关键。

能够看出service worker功能很是强大,特别是拦击全部请求、充当代理服务器这个功能,是强大而危险的。因此为了这个功能不被别有用心的人利用,service worker必须运行在HTTPS的Origin中,同时localhost也被认为是安全的,能够用于调试开发使用。

3. Service Worker的缓存

如前所述,service worker若是用于缓存则关键在于监听Fetch事件管理Cache资源,不过在使用它们以前,得先把service worker激活才行。而service worker的激活则要通过如下步骤:

  1. 浏览器发现当前页面注册了service worker(经过navigator.service.Worker.register('/sw.js'));
  2. 浏览器下载sw.js并执行,完成安装阶段;
  3. service worker等待Origin中其余worker失效,而后完成激活阶段;
  4. service worker生效,注意它的生效范围不是当前页面,而是整个Origin。可是只有是在register()成功以后打开的页面才受SW控制。因此执行注册脚本的页面一般须要重载一下,以便让SW得到彻底的控制。

下图是整个service worker的生命流程:
sw生命流程.png-38.4kB

下面用一个简单的例子来介绍service worker如何控制缓存,一般它在index.html中被注册:
代码清单:index.html

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <link href="style/style-1.css" rel-"stylesheet"> </head> <body> <img src="image/image-1.png" /> <script async src="js/script-1.js"></script> <script> if ('serivceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('Service worker registered successfully!')) .catch(err => console.log('Service worker failed to register!')); } </script> </body> </html>

能够看到这个页面有4个资源style-1.css image-1.png script-1.js以及sw.js。当页面中JS执行到register方法时,浏览器下载sw.js并根据sw.js内容准备安装Service worker。
代码清单: sw.js

let cacheName = 'indexCache'; self.addEventListener('install', event => { //waitUntil接受一个Promise,直到这个promise被resolve,安装阶段才算结束 event.waitUntil( caches.open(cacheName) .then(cache => cacheAll(['/style/style-1.css', '/image/image-1.png', '/script/script-1.js', ])) ); }); //监听activate事件,能够在这个事件里状况上个sw缓存的内容 self.addEventListener('activate', event => ...} //监听fetch事件,能够拦截全部请求并处理 self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(res => { //1. 若是请求的资源已被缓存,则直接返回 if (res) return res; //2. 没有,则发起请求并缓存结果 let requestClone = event.request.clone(); return fetch(requestClone).then(netRes => { if(!netRes || netRes.status !== 200) return netRes; let responseClone = netRes.clone(); caches.open(cacheName).then(cache => cache.put(requestClone, responseClone)); return netRes; }); }) ); });

能够看到,service worker在安装时就缓存了三个资源文件,若是下次该Origin下有页面对这三个资源发起请求,则会被Fetch事件拦截,而后直接用缓存返回。若是对其余资源发起请求,则会使用网络资源做为响应,并把这些资源再次存储起来。

能够看到仅用几十行代码就完成了一个很是强大的缓存控制功能,你还能够对特定的几个资源作本身的处理,取决你想怎么控制你的资源。目前还有一个问题尚待解决,那就是若是资源更新了,缓存该怎么办?目前有两种方法能够作到:

  1. 更新sw.js文件,一旦浏览器发现安装使用的sw.js是不一样的(经过计算hash值),浏览器就会从新安装service worker,你能够在安装激活的过程当中清空以前的缓存,这样浏览器就会使用服务器上最新的资源。
  2. 对资源文件进行版本控制,就像我上面的例子同样你能够用style-2.css来代替style-1.css,这样service worker就会使用新的资源并缓存它。固然版本号不该该这么简单,最好是使用文件的内容+修改时间+大小的hash值来做为版本号。

以上两种方法都是可靠的,第一种方法的可靠性由浏览器保证,第二种方法则是已经久经考验,目前大多数网站的静态资源更新策略都是用的相似于第二种方法的版本控制。这两种方法一般会混在一块儿使用,由于你在调整资源的版本号的时候,必需要更新sw.js中资源列表,致使sw.js文件自己就修改。

还有个问题须要注意,那就是sw.js自己也会被HTTP缓存策略缓存。经过对sw.js文件名进行版本控制,能够避免由于service worker安装文件被缓存而致使资源更新不及时的问题。

4. Service Worker的缓存延伸应用

前面说过,service worker的出现并非单纯的为解决精细化控制浏览器缓存问题的。它能充当代理服务器这一能力(经过拦截全部请求实现),可以实现HTTP缓存没法实现的功能:离线应用。由于在HTTP缓存策略下,若是一个资源过了服务器规定的到期时间,则必需要发起请求,一旦网络链接有问题,整个网站就会出现功能问题。而在service worker控制下的缓存,可以在代码中发现网络链接问题并直接返回缓存的资源。这种方式返回的响应对于浏览器来讲是透明的,它会认为该响应就是服务器发送回来的资源。

借助于上述能力以及service worker带来的推送能力,基于Web的应用已经可以媲美原生应用了。谷歌将这种Web应用称为PWA(Progressive Web Application)。

随着Web应用的功能愈来愈强大,安卓和IOS上套壳应用愈来愈多,最近微软也宣布win 10 上UWP应用能够采用PWA模式开发。至此跨平台应用开发的主流技术变得愈来愈清晰起来,业界在经历了Java-SWT,QT,Xamarin的尝试以后,HTML+CSS+Javascript这套始于浏览器的技术,已经成为跨平台应用开发的主流技术。

 
相关文章
相关标签/搜索