缓存详解

2023-05-16

前言

总括: 缓存从来都是前端的一个痛点,很多前端搞不清楚缓存到底是何物,从而给自己创造了一些麻烦,本文一如既往的用通俗易懂的文字和实例来讲述缓存,希望能让您有所得。

  • 原文博客地址: 缓存详解

  • 知乎专栏&&简书专题:前端进击者(知乎)

  • 博主博客地址:Damonare的个人博客

天青色等烟雨,而我在等你。

正文

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。

说实话,我起始真的不知道怎么去介绍缓存,所以引用了上面相对官方的定义。我想几乎每个开发者都碰到过缓存的问题吧,甚至有很多情况下我们会说这个问题已经修复了,你清理下缓存就好了。这篇文章我们就细细的来挖掘下缓存的种种轶事。

?缓存的种类

很多开发者习惯把cookie、webStorage以及IndexedDB存储的数据也称之为缓存,理由是都是保存在客户端的数据,没有什么区别。其实这是不严谨的,cookie的存在更多的是为了让服务端区别用户,webStorage和IndexedDB则更多用在保存具体的数据和在客户端存储大量结构化数据(文件/blobs)上面。

实际上所谓的缓存只有一种——它是请求资源的副本。试想一下,如果每一个资源我们客户端都会保存一份副本,这会怎么样?客户端会炸掉,开发者会疯掉!所以我们需要一份协议来处理缓存,可以让开发者控制缓存的建立和删除。谁呢?还能有谁,HTTP呗。HTTP协议里定义了很多关于缓存的请求和响应字段,这也是接下来我们重点要逼逼叨的对象,研究下究竟是哪些字段怎么影响缓存的。

纳尼?你问我为什么要缓存??

那就太容易说道了?,缓存好处有很多:

  1. 缓解服务器压力(不用每次去请求资源);
  2. 提升性能(打开本地资源速度当然比请求回来再打开要快得多);
  3. 减少带宽消耗(我相信你可以理解);

?‍♀️那么问题又来了,既然缓存这么好,如果我请求的服务器中间有代理也缓存了怎么办?代理服务器缓存了我的资源导致我没法从源服务器拿到最新的资源怎么办?HTTP当然也想到了这块的诉求。接下来我们也会逐层剖析。

?缓存在宏观上可以分成两类:私有缓存共享缓存。共享缓存就是那些能被各级代理缓存的缓存(咋觉得有点绕)。私有缓存就是用户专享的,各级代理不能缓存的缓存。

?微观上可以分下面三类:

1. 浏览器缓存

我相信只要你经常使用某个浏览器?(Chrome,Firefox,IE等),肯定知道这些浏览器在设置里面都是有个清除缓存功能,这个功能存在的作用就是删除存储在你本地磁盘上资源副本,也就是清除缓存。

缓存存在的意义就是当用户点击back按钮或是再次去访问某个页面的时候能够更快的响应。尤其是在多页应用的网站中,如果你在多个页面使用了一张相同的图片,那么缓存这张图片就变得特别的有用。?

2. 代理服务器缓存

代理服务器缓存原理和浏览器端类似,但规模要大得多,因为是为成千上万的用户提供缓存机制,大公司和大型的ISP提供商通常会将它们设立在防火墙上或是作为一个独立的设备来运营。(下文如果没有特殊说明,所有提到的缓存服务器都是指代理服务器。)

由于缓存服务器不是客户端或是源服务器的一部分,它们存在于网络中,请求路由必须经过它们才会生效,所以实际上你可以去手动设置浏览器的代理,或是通过一个中间服务器来进行转发,这样用户自然就察觉不到代理服务器的存在了。?

代理服务器缓存就是一个共享缓存,不只为一个用户服务,经常为大量用户使用,因此在减少相应时间和带宽使用方面很有效:因为同一个缓存可能会被重用多次。

3. 网关缓存

也被称为代理缓存或反向代理缓存,网关也是一个中间服务器,网关缓存一般是网站管理员自己部署,从让网站拥有更好的性能。?

CDNS(网络内容分发商)分布网关缓存到整个(或部分)互联网上,并出售缓存服务给需要的网站,比如国内的七牛云、又拍云都有这种服务。

4. 数据库缓存

数据库缓存是指当我们的应用极其复杂,表自然也很繁杂,我们必须进行频繁的进行数据库查询,这样可能导致数据库不堪重负,一个好的办法就是将查询后的数据放到内存中,下一次查询直接从内存中取就好了。关于数据库缓存本篇不会展开。?

?浏览器的缓存策略

缓存的目标:

  • 一个检索请求的成功响应: 对于 GET请求,响应状态码为:200,则表示为成功。一个包含例如HTML文档,图片,或者文件的响应;
  • 不变的重定向: 响应状态码:301;
  • 可用缓存响应:响应状态码:304,这个存在疑问,Chrome会缓存304中的缓存设置,Firefox;
  • 错误响应: 响应状态码:404 的一个页面;
  • 不完全的响应: 响应状态码 206,只返回局部的信息;
  • 除了 GET 请求外,如果匹配到作为一个已被定义的cache键名的响应;

以上,对于我们可以和应该缓存的目标有个了解。?

浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。

那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢❓响应头!响应头!响应头!重要的事情说三遍。✌️

我们看?:

Age:23146
Cache-Control:max-age=2592000
Date:Tue, 28 Nov 2017 12:26:41 GMT
ETag:W/"5a1cf09a-63c6"
Expires:Thu, 28 Dec 2017 05:27:45 GMT
Last-Modified:Tue, 28 Nov 2017 05:14:02 GMT
Vary:Accept-Encoding
复制代码

1. 强缓存阶段

以上请求头来自百度首页某个CSS文件的响应头。我去除了一些和缓存无关的字段,只保留了以上部分。我们来分析下,Expires是HTTP/1.0中的定义缓存的字段,它规定了缓存过期的一个绝对时间。Cache-Control:max-age=2592000是HTTP/1.1定义的关于缓存的字段,它规定了缓存过期的一个相对时间。优先级上当然是版本高的优先了,max-age > Expires

这就是强缓存阶段,当浏览器再次试图访问这个CSS文件,发现有这个文件的缓存,那么就判断根据上一次的响应判断是否过期,如果没过期,使用缓存。加载文件,OVER!✌️

Firefox浏览器表现为一个灰色的200状态码。

Chrome浏览器状态码表现为:

200 (from disk cache)或是200 OK (from memory cache)

**多说一点:**关于缓存是从磁盘中获取还是从内存中获取,查找了很多资料,得出了一个较为可信的结论:Chrome会根据本地内存的使用率来决定缓存存放在哪,如果内存使用率很高,放在磁盘里面,内存的使用率很高会暂时放在内存里面。这就可以比较合理的解释了为什么同一个资源有时是from memory cache有时是from disk cache的问题了。

那么当这个CSS文件过期了怎么办?ETagLast-Modified就该闪亮登场了。

先说Last-Modified,这个字段是文件最后一次修改的时间;

ETag呢?ETag是对文件的一个标记,嗯,可以这么说,具体生成方式HTTP并没有给出一个明确的方式,所以理论上只要不会重复生成方式无所谓,比如对资源内容使用抗碰撞散列函数,使用最近修改的时间戳的哈希值,甚至只是一个版本号。

####2. 协商缓存阶段

利用这两个字段浏览器可以进入协商缓存阶段,当浏览器再次试图访问这个CSS文件,发现缓存过期,于是会在本次请求的请求头里携带If-Moified-SinceIf-None-Match这两个字段,服务器通过这两个字段来判断资源是否有修改,如果有修改则返回状态码200和新的内容,如果没有修改返回状态码304,浏览器收到200状态码,该咋处理就咋处理(相当于首次访问这个文件了),发现返回304,于是知道了本地缓存虽然过期但仍然可以用,于是加载本地缓存。然后根据新的返回的响应头来设置缓存。(这一步有所差异,发现不同浏览器的处理是不同的,chrome会为304设置缓存,firefox则不会)?

具体两个字段携带的内容如下(分别和上面的Last-ModifiedETag携带的值对应):

If-Moified-Since: Tue, 28 Nov 2017 05:14:02 GMT
If-None-Match: W/"5a1cf09a-63c6"
复制代码

到这协商缓存结束。

3. 启发式缓存阶段

我们把上面的响应头改下:

Age:23146
Cache-Control: public
Date:Tue, 28 Nov 2017 12:26:41 GMT
Last-Modified:Tue, 28 Nov 2017 05:14:02 GMT
Vary:Accept-Encoding
复制代码

发现没?浏览器用来确定缓存过期时间的字段一个都没有!那该怎么办?有人可能会说下次请求直接进入协商缓存阶段,携带If-Moified-Since呗,不是的,浏览器还有个启发式缓存阶段?

根据响应头中2个时间字段 Date 和 Last-Modified 之间的时间差值,取其值的10%作为缓存时间周期。

这就是启发式缓存阶段。这个阶段很容让人忽视,但实际上每时每刻都在发挥着作用。所以在今后的开发过程中如果遇到那种默认缓存的坑,不要叫嚣,不要生气,浏览器只是在遵循启发式缓存协议而已。

我画了下面这张图,来解释浏览器整个缓存策略的过程:

?对于缓存策略介绍到这,接下来再细细分析不同的HTTP首部字段的内容,以及它们之间的关系。

?HTTP中和缓存相关的首部字段

HTTP报文是什么呢?就是HTTP报文,这是一个概念,主要由以下两部分构成:

  1. 首部(header):包含了很多字段,比如:cookie、缓存、报文大小、报文格式等等);
  2. 主体(body):HTTP请求真正要传输的部分,比如:一个HTML文档,一个js文件;

以上我们知道浏览器对于缓存的处理过程,也简单的提到了几个相关的字段。?接下来我们具体看下这几个字段:

1. 通用首部字段

字段名称说明
Cache-Control控制缓存具体的行为
PragmaHTTP1.0时的遗留字段,当值为"no-cache"时强制验证缓存
Date创建报文的日期时间(启发式缓存阶段会用到这个字段)

2. 响应首部字段

字段名称说明
ETag服务器生成资源的唯一标识
Vary代理服务器缓存的管理信息
Age资源在缓存代理中存贮的时长(取决于max-age和s-maxage的大小)

3. 请求首部字段

字段名称说明
If-Match条件请求,携带上一次请求中资源的ETag,服务器根据这个字段判断文件是否有新的修改
If-None-Match和If-Match作用相反,服务器根据这个字段判断文件是否有新的修改
If-Modified-Since比较资源前后两次访问最后的修改时间是否一致
If-Unmodified-Since比较资源前后两次访问最后的修改时间是否一致

4. 实体首部字段

字段名称说明
Expires告知客户端资源缓存失效的绝对时间
Last-Modified资源最后一次修改的时间

?浏览器缓存控制

HTTP/1.1一共规范了47种首部字段,而和缓存相关的就有以上12个之多。接下来的两个小节会一个一个介绍给大家。?

1. Cache-Control

通过cache-control的指令可以控制告诉客户端或是服务器如何处理缓存。这也是11个字段中指令最多的一个,我们先来看看请求指令

指令参数说明
no-cache强制源服务器再次验证
no-store不缓存请求或是响应的任何内容
max-age=[秒]缓存时长,单位是秒缓存的时长,也是响应的最大的Age值
min-fresh=[秒]必需期望在指定时间内响应仍然有效
no-transform代理不可更改媒体类型
only-if-cached从缓存获取
cache-extension-新的指令标记(token)

响应指令

指令参数说明
public任意一方都能缓存该资源(客户端、代理服务器等)
private可省略只能特定用户缓存该资源
no-cache可省略缓存前必须先确认其有效性
no-store不缓存请求或响应的任何内容
no-transform代理不可更改媒体类型
must-revalidate可缓存但必须再向源服务器进确认
proxy-revalidate要求中间缓存服务器对缓存的响应有效性再进行确认
max-age=[秒]缓存时长,单位是秒缓存的时长,也是响应的最大的Age值
s-maxage=[秒]必需公共缓存服务器响应的最大Age值
cache-extension-新指令标记(token

请注意no-cache指令很多人误以为是不缓存,这是不准确的,no-cache的意思是可以缓存,但每次用应该去想服务器验证缓存是否可用。no-store才是不缓存内容。另外部分指令也可以组合使用,比如:

Cache-Control: max-age=100, must-revalidate, public
复制代码

上面指令的意思是缓存的有效时间为100秒,之后访问需要向源服务器发送请求验证,此缓存可被代理服务器和客户端缓存。

2. Pragma

这是HTTP/1.0里面的一个字段,但优先级很高,测试发现,Chrome和Firefox中Pragma的优先级高于Cache-Control和Expires,为了向下兼容,这个字段依然发挥着它的作用。?一般可能我们会这么用:

<meta http-equiv="Pragma" content="no-cache">
复制代码

Pragma属于通用首部字段,在客户端上使用时,常规要求我们往html上加上上面这段meta元标签(而且可能还得做些hack放到body后面去。

事实上这种禁用缓存的形式用处很有限:

  1. 仅有IE才能识别这段meta标签含义,其它主流浏览器仅能识别Cache-Control: no-store的meta标签(见出处)
  2. 在IE中识别到该meta标签含义,并不一定会在请求字段加上Pragma,但的确会让当前页面每次都发新请求(仅限页面,页面上的资源则不受影响)。——浅谈浏览器http的缓存机制

读者可以自行拷贝后面模拟服务端决策的代码进行测试。

服务端响应添加'Pragma': 'no-cache',浏览器表现行为和强制刷新类似。

3. Expires

这又是一个HTTP/1.0的字段,上面也说过了定义的是缓存到期的绝对时间。

同样,我们也可以在html文件里直接使用:

<meta http-equiv="expires" content="Thu, 30 Nov 2017 11:17:26 GMT">
复制代码

如果设置的是已经过去的时间会怎样呢?YES!!!则刷新页面会重新发送请求。

**Pragma禁用缓存,如果又给Expires定义一个还未到期的时间,那么Pragma字段的优先级会更高。**?

?Expires有一个很大的弊端,就是它返回的是服务器的时间,但判断的时候用的却是客户端的时间,这就导致Expires很被动,因为用户有可能改变客户端的时间,导致缓存时间判断出错,这也是引入Cache-Control:max-age指令的原因之一。

4. Last-Midified

接下来这几个字段都是校验字段,或者说是在协商缓存阶段发挥作用的字段。第一个就是Last-modified,这个字段不光协商缓存起作用,在启发式缓存阶段同样起到至关重要的作用。

在浏览器第一次请求某一个URL时,服务器端的返回状态码会是200,响应的实体内容是客户端请求的资源,同时有一个Last-Modified的属性标记此文件在服务器端最后被修改的时间。like this:

Last-Modified : Fri , 12 May 2006 18:53:33 GMT
复制代码
If-Modified-Since

当浏览器第二次请求这个URL的时候,根据HTTP协议规定,浏览器会把第一次Last-Modified的值存储在If-Modified-Since里面发送给服务端来验证资源有没有修改。like this:

If-Modified-Since : Fri , 12 May 2006 18:53:33 GMT
复制代码

服务端通过If-Modified-Since字段来判断在这两次访问期间资源有没有被修改过,从而决定是否返回完整的资源。如果有修改正常返回资源,状态码200,如果没有修改只返回响应头,状态码304,告知浏览器资源的本地缓存还可用。

用途:

  • 验证本地缓存是否可用
If-Unmodified-Since

这个字段字面意思和If-Modified-Since相反,但处理方式并不是相反的。如果文件在两次访问期间没有被修改则返回200和资源,如果文件修改了则返回状态码412(预处理错误)。

用途:

  • 与含有 If-Range消息头的范围请求搭配使用,实现断点续传的功能,即如果资源没修改继续下载,如果资源修改了,续传的意义就没有了。
  • POST、PUT请求中,优化并发控制,即当多用户编辑用一份文档的时候,如果服务器的资源已经被修改,那么在对其作出编辑会被拒绝提交。

?Last-Modified有几个缺点:没法准确的判断资源是否真的修改了,比如某个文件在1秒内频繁更改了多次,根据Last-Modified的时间(单位是秒)是判断不出来的,再比如,某个资源只是修改了,但实际内容并没有发生变化,Last-Modified也无法判断出来,因此在HTTP/1.1中还推出了ETag这个字段?

5. ETag

服务器可以通过某种自定的算法对资源生成一个唯一的标识(比如md5标识),然后在浏览器第一次请求某一个URL时把这个标识放到响应头传到客户端。服务器端的返回状态会是200。

ETag: abc-123456
复制代码

ETag的值有可能包含一个 W/ 前缀,来提示应该采用弱比较算法(这个是画蛇添足,因为 If-None-Match 用且仅用这一算法)。?

If-None-Match

If-None-Match和If-Modified-Since同时存在的时候If-None-Match优先级更高。

当浏览器第二次请求这个URL的时候,根据HTTP协议规定,浏览器回把第一次ETag的值存储在If-None-Match里面发送给服务端来验证资源有没有修改。like this:

If-None-Match: abc-123456
复制代码

Get请求中,当且仅当服务器上没有任何资源的ETag属性值与这个首部中列出的相匹配的时候,服务器端会才返回所请求的资源,响应码为200。如果没有资源的ETag值相匹配,那么返回304状态码。

POST、PUT等请求改变文件的请求,如果没有资源的ETag值相匹配,那么返回412状态码。

If-Match

在请求方法为 GET) 和 HEAD的情况下,服务器仅在请求的资源满足此首部列出的 ETag之一时才会返回资源。而对于 PUT或其他非安全方法来说,只有在满足条件的情况下才可以将资源上传。

用途:

  • For GET和 HEAD 方法,搭配 Range首部使用,可以用来保证新请求的范围与之前请求的范围是对同一份资源的请求。如果 ETag 无法匹配,那么需要返回 416(范围请求无法满足) 响应。
  • 对于其他方法来说,尤其是 PUT, If-Match 首部可以用来避免更新丢失问题。它可以用来检测用户想要上传的不会覆盖获取原始资源之后做出的更新。如果请求的条件不满足,那么需要返回412(预处理错误) 响应。

当然和Last-Modified相比,ETag也有自己的缺点,比如由于需要对资源进行生成标识,性能方面就势必有所牺牲。?

关于强校验和弱校验:

ETag 1ETag 2Strong ComparisonWeak Comparison
W/"1"W/"1"no matchmatch
W/"1"W/"2"no matchno match
W/"1""1"no matchmatch
"1""1"matchmatch

?服务端缓存控制

ExpiresCache-Control:max-age=xxx同时存在的时候取决于缓存服务器应用的HTTP版本。应用HTTP/1.1版本的服务器会优先处理max-age,忽略Expires,而应用HTTP/1.0版本的缓存服务器则会优先处理Expires而忽略max-age。接下来看下和缓存服务器相关的两个字段。

6. Vary

Vary用来做什么的呢?试想这么一个场景:在某个网页中网站提供给移动端的内容是不同的,怎么让缓存服务器区分移动端和PC端呢?不知道你是否注意,浏览器在每次请求都会携带UA字段来表明来源,所以我们可以利用User-Agent字段来区分不同的客户端,用法如下:

Vary: User-Agent
复制代码

再比如,源服务器启用了gzip压缩,但用户使用了比较旧的浏览器,不支持压缩,缓存服务器如何返回?就可以这么设定:

Vary: Accept-Encoding
复制代码

当然,也可以这么用:

Vary: User-Agent, Accept-Encoding
复制代码

这意味着缓存服务器会以User-AgentAccept-Encoding两个请求首部字段来区分缓存版本。根据请求头里的这两个字段来决定返回给客户端什么内容。

7. Age

这个字段说的是资源在缓存服务器存在的时长,前面也说了Cache-Control: max-age=[秒]就是Age的最大值。

这个字段存在的意义是什么呢?用来区分请求的资源来自源服务器还是缓存服务器的缓存的。

?但得结合另一个字段来进行判断,就是Date,Date是报文创建的时间。

Date

如果按F5频繁刷新发现响应里的Date没有改变,就说明命中了缓存服务器的缓存以下面的一个响应为?:

Accept-Ranges: bytes
Age: 1016859
Cache-Control: max-age=2592000
Content-Length: 14119
Content-Type: image/png
Date: Fri, 01 Dec 2017 12:27:25 GMT
ETag: "5912bfd0-3727"
Expires: Tue, 19 Dec 2017 17:59:46 GMT
Last-Modified: Wed, 10 May 2017 07:22:56 GMT
Ohc-Response-Time: 1 0 0 0 0 0
Server: bfe/1.0.8.13-sslpool-patch
复制代码

如上图来自百度首页某个图片的响应字段。我们可以看到Age=1016859,说明这个资源已经在缓存服务器存在了1016859秒。如果文件被修改或替换,Age会重新由0开始累计。

Age消息头的值通常接近于0。表示此消息对象刚刚从原始服务器获取不久;其他的值则是表示代理服务器当前的系统时间与此应答消息中的通用消息头 Date的值之差。

上面这个结论归结为一个等式就是:

静态资源Age + 静态资源Date = 原服务端Date
复制代码

?用户操作行为对缓存的影响

搜索了很久有没有关于这方面的权威总结,最后竟然在百度百科找到了也是很惊讶,我自己加了一条用户强制刷新操作浏览器的反应。强制刷新,window下是Ctrl+F5,mac下就是command+shift+R操作了。:relieved:

操作说明
打开新窗口如果指定cache-control的值为private、no-cache、must-revalidate,那么打开新窗口访问时都会重新访问服务器。而如果指定了max-age值,那么在此值内的时间里就不会重新访问服务器,例如:Cache-control: max-age=5 表示当访问此网页后的5秒内不会去再次访问服务器.
在地址栏回车如果值为private或must-revalidate,则只有第一次访问时会访问服务器,以后就不再访问。如果值为no-cache,那么每次都会访问。如果值为max-age,则在过期之前不会重复访问。
按后退按扭如果值为private、must-revalidate、max-age,则不会重访问,而如果为no-cache,则每次都重复访问.
按刷新按扭无论为何值,都会重复访问.(可能返回状态码:200、304,这个不同浏览器处理是不一样的,FireFox正常,Chrome则会启用缓存(200 from cache))
按强制刷新按钮当做首次进入重新请求(返回状态码200)

来自百度百科

:wink:如果想在浏览器点击“刷新”按钮的时候不让浏览器去发新的验证请求呢?办法找到一个,知乎上面一个回答,在页面加载完毕后通过脚本动态地添加资源:

$(window).load(function() {
  	var bg='http://img.infinitynewtab.com/wallpaper/100.jpg';
  	setTimeout(function() {
    	$('#bgOut').css('background-image', 'url('+bg+')');
  	},0);
});
复制代码

来自知乎

?HTML5的缓存

这部分准备的说应该叫离线存储。现在比较普遍用的是Appcache,但Appcache已经从web标准移除了,在可预见的未来里,ServiceWorker可能会是一个比较适合的解决方案。

1. Appcache

这是HTML5的一个新特性,通过离线存储达到用户在没有网络连接的情况下也能访问页面的功能。离线状态下即使用户点击刷新都能正常加载文档。

使用方法如下,在HTML文件中引入appcache文件:

<!DOCTYPE html>
<html manifest="manifest.appcache">
<head>
  <meta charset="UTF-8">
  <title>***</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
复制代码

?web 应用中的 manifest 特性可以指定为缓存清单文件的相对路径或一个绝对 URL(绝对 URL 必须与应用同源)。缓存清单文件可以使用任意扩展名,但传输它的 MIME 类型必须为 text/cache-manifest。

**注意:**在 Apache 服务器上,若要设置适用于清单(.appcache)文件的 MIME 类型,可以向根目录或应用的同级目录下的一个 .htaccess 文件中增加 AddType text/cache-manifest .appcache

CACHE MANIFEST
# 注释:需要缓存的文件,无论在线与否,均从缓存里读取
# v1 2017-11-30
# This is another comment
/static/logo.png

# 注释:不缓存的文件,始终从网络获取
NETWORK:
example.js

# 注释:获取不到资源时的备选路径,如index.html访问失败,则返回404页面
FALLBACK:
index.html 404.html
复制代码

上面就是一个完整的缓存清单文件的示例。

**注意:**主页一定会被缓存起来的,因为AppCache主要是用来做离线应用的,如果主页不缓存就无法离线查看了,因此把index.html添加到NETWORK中是不起效果的。

实际上这个特性已经web标准中删除,但现在为止还有很多浏览器支持它,所以这里提一下。

你可以用最新的Firefox(版本 57.0.1)测试下,控制台会有这么一行字?:

程序缓存 API(AppCache)已不赞成使用,几天后将被移除。需离线支持请尝试使用 Service Worker。

最新Chrome(版本 62.0.3202.94)倒是没有这个警告。?

AppCache之所以不受待见我想了下面几个原因:

  1. 一旦使用了manifest后,没办法清空这些缓存,只能更新缓存,或者得用户自己去清空浏览器的缓存;
  2. 假如更新的资源中有一个资源更新失败了,那么所有的资源就会全部更新失败,将用回上一版本的缓存;
  3. 主页会被强制缓存(使用了manifest的页面),并且无法清除;
  4. appache文件可能会无法被及时更新,因为各大浏览器对于appcache文件的处理方式不同;
  5. 以上几个弊端一旦出问题,会让用户抓狂更会让开发者抓狂!

2. Service Worker

Service worker还是一个实验性的功能,线上环境不推荐使用。?这里大概介绍一下。

Service worker本质上充当Web应用程序与浏览器之间的代理服务器。

?首先讲个小故事:

我们都知道浏览器的js引擎处理js是单线程的,它就好像一个大Boss高高在上,同一个时间它只做一个事情(就是那么傲娇),基于这个弊端,W3C(HR)给大Boss招聘了一个秘书(web worker),大Boss可以把琐碎的事情交给秘书web worker去做,做完了发个微信(postMessage)通知大Boss,大Boss通过onmessage来获取秘书web worker做的事情的结果。傍晚时分,下班时间到!大Boss回家哄儿子了,秘书也出去约会去了,没人加班了!这怎么行!W3C(HR)又提出了招个程序?的想法的想法,OK,Service Worker应聘成功!于是,程序?就坚持在工作岗位上了,从此开启没完没了的加班之路。总的来说这只猿的工作是这样的:

  • 后台数据同步
  • 响应来自其它源的资源请求
  • 集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据
  • 在客户端进行CoffeeScript,LESS,CJS/AMD等模块编译和依赖管理(用于开发目的)
  • 后台服务钩子
  • 自定义模板用于特定URL模式
  • 性能增强,比如预取用户可能需要的资源

——Service Worker API

注意:Service workers之所以优于以前同类尝试(如上面提到的AppCache)),是因为它们无法支持当操作出错时终止操作。Service workers可以更细致地控制每一件事情。如何控制的呢?

Service workers利用了ES6中比较重要的特性Promise,并且在拦截请求的时候使用的是新的fetch API,之所以使用fetch就是因为fetch返回的是Promise对象。可以说Service workers重要组成部分就是三块:事件、Promise和Fetch请求。OK,talk is cheap,show you the code。?

首先我们看下app.js文件:告诉浏览器注册某个JavaScript文件为service worker,检查service worker API是否可用,如果可用就注册service worker:

//使用 ServiceWorkerContainer.register()方法首次注册service worker。
if (navigator.serviceWorker) {
  	navigator.serviceWorker.register('./sw.js', {scope: './'})
      	.then(function (registration) {
          	console.log(registration);
      	})
      	.catch(function (e) {
          	console.error(e);
      	});
} else {
  	console.log('该浏览器不支持Service Worker');
}
复制代码

再来看看具体作为service worker的文件sw.js,例子如下:

const CACHE_VERSION = 'v1'; // 缓存文件的版本
const CACHE_FILES = [ // 需要缓存的文件
	'./test.js',
	'./app.js',
	'https://code.jquery.com/jquery-3.0.0.min.js'
];

self.addEventListener('install', function (event) { // 监听worker的install事件
    event.waitUntil( // 延迟install事件直到缓存初始化完成
        caches.open(CACHE_VERSION)
		.then(function (cache) {
			console.log('缓存打开');
			return cache.addAll(CACHE_FILES);
		})
    );
});

self.addEventListener('activate', function(event) {// 监听worker的activate事件
    event.waitUntil(// 延迟activate事件直到
        caches.keys().then(function(keys) {
            return Promise.all(keys.map(function(key, i){
                if(key !== CACHE_VERSION){
                    return caches.delete(keys[i]); // 清除旧版本缓存
                }
            }))
        })
    )
});

self.addEventListener('fetch', function(event) { // 截取页面的资源请求
    event.respondWith(
        caches.match(event.request).then(function(res) { // 判断缓存是否命中
            if (res) { // 返回缓存中的资源
                return res;
            }
            _request(event); // 执行请求备份操作
        })
    )
});

function _request(event) {
    var url = event.request.clone();
    return fetch(url).then(function(res) {// 使用fetch请求线上资源
        // 错误判断
        if (!res || res.status !== 200 || res.type !== 'basic') {
            return res;
        }

        var response = res.clone(); // 创建了一个响应对象的克隆,储藏在一个单独的变量中

        caches.open(CACHE_VERSION).then(function(cache) {// 缓存从线上获取的资源
            cache.put(event.request, response);
        });
        return res;
    })
}
复制代码

清除一个Service Worker也很简单:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', {scope: './'}).then(function(registration) {
    // registration worked
    console.log('Registration succeeded.');
    registration.unregister().then(function(boolean) {
      // if boolean = true, unregister is successful
    });
  }).catch(function(error) {
    // registration failed
    console.log('Registration failed with ' + error);
  });
};
复制代码

相对AppCache来说,Service Worker的API增多了不少,用法也更复杂了些,但看得出Service Worker才是未来,对于web app来说,更是如虎添翼。现在支持Service Worker的浏览器除了Chrome和Firefox,最近新添一个生力军——Safari也支持Service Worker了。期待它在未来大放异彩吧。?

?模拟实现服务端决策

如下,使用node原生代码简单的模拟下服务器发送响应的过程,包括对于协商缓存的处理过程:

var http = require('http');
var fs = require('fs');
var url = require('url');

process.env.TZ = 'Europe/London';

let tag = '123456';

http.createServer( function (request, response) {  

   var pathname = url.parse(request.url).pathname;

   	console.log("Request for " + pathname + " received.");
   	const fileMap = {
	   'js': 'application/javascript; charset=utf-8',
	   'html': 'text/html',
	   'png': 'image/png',
	   'jpg': 'image/jpeg',
	   'gif': 'image/gif',
	   'ico': 'image/*',
       'appcache': 'text/cache-manifest'
   	}
   	fs.readFile(pathname.substr(1), function (err, data) {
		if (request.headers['if-none-match'] === tag) {
			response.writeHead(304, {
				'Content-Type': fileMap[pathname.substr(1).split('.')[1]],
				'Expires': new Date(Date.now() + 30000),
				'Cache-Control': 'max-age=10, public',
				'ETag': tag,
				'Last-Modified': new Date(Date.now() - 30000),
				'Vary': 'User-Agent'
			});
	   } else {             
			response.writeHead(200, {
				'Content-Type': fileMap[pathname.substr(1).split('.')[1]],
				'Cache-Control': 'max-age=10, public',
				'Expires': new Date(Date.now() + 30000),
				'ETag': tag,
				'Last-Modified': new Date(Date.now() - 30000),
				'Vary': 'User-Agent'
			});
			response.write(fs.readFileSync(pathname.substr(1)));        
      	}
      	response.end();
   	});   
}).listen(8081);
复制代码

如上代码。如果你没使用过node,拷贝下代码存为file.js,安装node,命令行输入node file.js,可以在同目录下建立index.html文件,在html文件中引用一些图片,CSS等文件,浏览器输入localhost:8081/index.html进行模拟。?

?关于缓存的一些问答

1. 问题:请求被缓存,导致新代码未生效

解决方案:

  • 服务端响应添加Cache-Control:no-cache,must-revalidate指令;
  • 修改请求头If-modified-since:0If-none-match
  • 修改请求URL,请求URL后加随机数,随机数可以是时间戳,哈希值,比如:http://damonare.cn?a=1234

2. 问题:服务端缓存导致本地代码未更新

解决方案:

  • 合理设置Cache-Control:s-maxage指令;
  • 设置Cache-Control:private指令,防止代理服务器缓存资源;
  • CDN缓存可以使用管理员设置的缓存刷新接口进行刷新;

3. 问题: Cache-Control: max-age=0 和 no-cache有什么不同

回答:

max-age=0no-cache应该是从语气上不同。max-age=0是告诉客户端资源的缓存到期应该向服务器验证缓存的有效性。而no-cache则告诉客户端使用缓存前必须向服务器验证缓存的有效性。

后记

参考文档:

  • 浅谈浏览器http的缓存机制
  • Cache-Control
  • What's the difference between Cache-Control: max-age=0 and no-cache?
  • Header Field Definitions
  • Caching Tutorial
  • If-Unmodified-Since
  • If-Match
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

缓存详解 的相关文章

  • 上云有风险 公有云选型小心进坑

    本文讲的是上云有风险 公有云选型小心进坑 IT168 云计算 市场商业的运作规则就是大鱼吃小鱼小鱼吃虾米 xff0c 适者生存 在公有云领域 xff0c 如果你没有实力长时间支撑一个企业运营微利甚至不盈利 xff0c 那么云平台倒下一定是必
  • RabbitMQ消息可靠性分析

    欢迎支持笔者新书 xff1a RabbitMQ实战指南 以及关注微信公众号 xff1a 朱小厮的博客 Introduction 有很多人问过我这么一类问题 xff1a RabbitMQ如何确保消息可靠 xff1f 很多时候 xff0c 笔者
  • debian关闭图形界面_Debian Linux进入不了图形界面的折腾

    首先我的操作系统是Debian Linux xff0c 我Debian用的是Xfce桌面环境 xff0c 昨天我用了命令apt get install gnome 安装了一堆软件之后重启 xff0c 就进入不了图形界面了 xff0c 只能进
  • 计算机网络操作系统的主要功能和类型,操作系统的五大管理功能和四大分类

    操作系统是管理计算机硬件资源 xff0c 控制其他程序运行并为用户提供交互操作界面的系统软件的集合 操作系统是计算机系统的关键组成部分 xff0c 负责管理与配置内存 决定系统资源供需的优先次序 控制输入与输出设备 操作网络与管理文件系统等
  • 使用Formik轻松开发更高质量的React表单(二)使用指南

    一个基本的例子 设想你要开发一个可以编辑用户数据的表单 不过 xff0c 你的用户API端使用了具有类似下面的嵌套对象表达 xff1a id string email string social facebook string twitte
  • Media Foundation基本概念

    Media Foundation Media Foundation xff08 简称MF xff09 是微软用来替换DirectShow的视频API xff0c 从Vista开始支持 xff0c 直到现在的Windows 8 xff0c W
  • 算法公式 - 图形化

    在定制 数学公式的时候 可能 希望有个直观的 展现 http www wolframalpha com 我们前一段时间用到的 推荐分值 90 天 递减公式 xff0c 如果这东西早发现 公式就不会错了 xff01 目前我们递减的公式 y 6
  • windows平台使用SecureCRT+Xming实现图形界面来执行linux平台的一些图形程序

    需求分析 xff1a windows平台下使用ssh远程连接linux服务器 xff0c 实现图形界面来执行linux平台的一些程序 xff0c 如system config kickstart xeyes 等 实验环境 xff1a 服务器
  • 上线:准备和部署软件包时开发和运维的角色

    按照发布流程正确的部署软件 二进制代码和与之相关的配置文件 到你的开发 测试 验收或产品环境 xff08 DTAP xff09 是一项复杂的任务 xff0c 涉及到众多部门和团队 不幸的是 xff0c 在许多组织中这项关键的流程还是费时并容
  • TortoiseGit安装与配置

    2019独角兽企业重金招聘Python工程师标准 gt gt gt TortoiseGit 简称 tgit 中文名海龟Git 海龟Git只支持神器 Windows 系统 有一个前辈海龟SVN TortoiseSVN和TortoiseGit都
  • 在CentOS 7中Samba服务安装和配置

    这篇指南介绍了如何在CentOS7中配置匿名和安全的Samba服务器 Samba是一个开源 自由软件套件 xff0c 提供无缝的文件和打印服务SMB CIFS客户端 Samba是免费的 xff0c 不像其他的SMB CIFS的实现着 Sam
  • 如何清除团队中的“害群之马”?(下篇)

    阅读更多系列文章请访问我的GitHub博客 这篇文章来自于一位同事面试时被问到的一个问题 xff1a 如果团队内有老员工 xff0c 能力不强又喜欢偷懒 xff0c 怎么办 xff1f 对于这类问题 xff0c 最简单的回答就是 xff1a
  • oracle 备份失败 RMAN-03002,RMAN-00569,RMAN-00571等处理方法

    备份失败 1 备份失败信息 xff1a 39 52 备份 200 3 oracle 数据 开始执行 11 39 54 NICEGAIN SW741B 开始分析备份数据源 xff0c 请稍候 11 39 54 Oracle Database
  • KVM 开启嵌套虚拟化

    问题 在 CentOS KVM 上启动虚拟机来部署 OpenStack 测试环境 xff0c 在启动具有 CPU 绑定 NUMA 亲和的虚拟机时触发错误 xff1a libvirtError Requested operation is n
  • VNC指定用户屏幕号登录(Oracle安装时使用)

    由于oracle在安装的过程中需要从oracle切换到root用户 xff0c 为了在安装的时候 xff0c 可以远程安装 xff0c 故对VNC进行配置 vim etc sysconfig vncservers定义root的屏幕号为1 o
  • ubuntu 18.04屏幕共享

    2019独角兽企业重金招聘Python工程师标准 gt gt gt windows7系统通过连接ubuntu 18 04屏幕共享 xff0c 来访问Ubuntu桌面 1 安装 xff1a sudo apt install xrdp 2 编辑
  • Git:分支管理

    本文目录 1 使用方法 1 1 常规开发 1 2 热修复 1 3 净化Test分支 2 冲突解决 3 分支说明 3 1 master分支 3 2 release分支 3 3 test分支 3 4 feature分支 3 5 hotfix分支
  • [转载]打造自己喜欢的Linux桌面----archlinux

    原文地址 xff1a 打造自己喜欢的Linux桌面 archlinux 作者 xff1a 三尺椴 打造自己的Linux桌面 Archlinux 2011 01 16 文 s cd xff08 常用桌面组合 Archlinux 43 fvwm
  • 操作日志工具类

    package com util import java util Date import java util UUID import javax servlet http HttpServletRequest import org spr
  • 其他窗体赋值给comboBox实现值的回显,并使赋的值处于选中状态(根据text获取selectedindex)...

    Form1 发货单位的这个下拉框comboBox1已经绑定数据库test表的name字段 xff0c 里面有很多单位名称 比如有 xff1a 甲公司 乙公司 1 Form1的comboBox1首先绑定数据库的数据表test using SQ

随机推荐

  • 树莓派之OLED12864视频播放—BadApple

    代码地址如下 xff1a http www demodashi com demo 13218 html 概述 本篇教程讲述了使用树莓派驱动OLED12864液晶屏 并在液晶屏上播放动画和视频 硬件平台 树莓派一台 RaspberryPi 2
  • 帮你解决无法安装ia32-libs 的问题

    在安装安装wpsforlinux等软件时 xff0c 我们要经常要运行 代码 sudo apt get install ia32 libs 问题 xff1a 解决办法 xff1a 一 在终端运行 sudo apt get install g
  • 爬虫带你了解一下Golang的市场行情

    了解一下Golang的市场行情 项目地址 xff1a https github com go crawler 如果对你有所帮助 xff0c 欢迎 Star xff0c 给文章来波赞 xff0c 这样可以让更多的人看见 目标 在工作中 Gol
  • C语言如何分离一个数的高低位,如何将2个字节变成一个字节

    关于这个概念 xff0c 是我从工作中学习的 xff0c 虽然在读书的时候就应该要掌握 xff0c 但是在开发中 xff0c 这项技能尤其重要 我是做嵌入式开发的 xff0c 在嵌入式开发过程中 xff0c 如何对数据操作必然是不可缺少的问
  • 你要的免费Proxy资源全在这里了

    地址 xff1a github com derekhe Pro 介绍 在 爬虫实战 xff1a 从数据到产品 一书中 xff0c 我讲到了一个基于ProxyBroker的代理池 经过我的长时间的实践 xff0c 这个代理池用起来非常的方便和
  • 短视频App源码:如何搭建短视频社区

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 短视频App源码 xff1a 如何搭建短视频社区 随着国内移动互联网的发展 xff0c 中国的移动互联网时代已经来临 xff0c 以快手 抖音为主的短视频平台迅速火热起来
  • mysql 百万级数据的模糊查询 优化 笔记

    最近老大给了一个需求 xff0c 是要写一个姓名的模糊查询 问题很简单 xff0c 难度在于这张表有将近500W条数据 如果要做中文的模糊查询 xff0c 效率简直惨不忍睹 网上查了一下资料 xff0c 发现全文索引挺符合我的需要的 结果
  • 如何在Ubuntu 18.04上创建启用了Sudo的新用户[快速入门]

    介绍 Introduction The sudo command provides a mechanism for granting administrator privileges ordinarily only available to
  • Sql语句批量更新数据(多表关联)

    最近在项目中遇到一个问题 xff0c 原来设计的功能是不需要一个特定的字段值depid的 xff0c 但是新的功能需要根据depid来展现 xff0c 于是出现了这样一个问题 xff0c 新增加的数据都有正确的depid 而原来的大量的数据
  • Matlab M文件“程序块”注释方法

    方法一 xff1a 注释语句的快捷键是Ctrl 43 R 取消注释的快捷键是Ctrl 43 T 并且支持一次注释 xff08 或者取消注释 xff09 多行语句 选定要注释 xff08 或者取消注释 xff09 的那些语句 xff0c 然后
  • 如何开发高性能低成本的网站之技术选择

    每个企业都是慢慢发展起来的 xff0c 在起步阶段成本是一个不得不考虑的重大问题 直接入正题 xff1a 前台框架 ASP NET MVC 43 Jquery 43 Json 43 Flash ASP NET MVC 高性能速度快 xff0
  • 密码学题库

    参考题库 一 选择题 1 第一个实用的 迄今为止应用最广的公钥密码体制是 A A RSA B Elgamal C ECC D NTRU 2 一个密码系统至少由明文 密文 加密算法和解密算法 密钥五部分组成 xff0c 而其安全性是由 xff
  • 【linux】linux 下 shell命令 执行结果赋值给变量【两种方式】

    方法1 xff1a 通用方法 使用Tab键上面的反引号 例子如下 xff1a find命令 模糊查询在 apps swapping目录下 查找 文件名中包含swapping并且以 jar结尾的文件 使用反引号 引住命令 xff0c 然后使用
  • Octet 和 Byte 的区别

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 关于程序的文章中 Octet 和 Byte是常见的词汇 xff0c 他们都表示8 bit 在读RFC或网络设备文档时 xff0c 经常见到Octet这量词来指代8位 xff
  • 云计算的弹性

    云计算最大的优势就在于弹性 目前 xff0c 阿里云已拥有在数分钟内开出一家中型互联网公司所需要的IT资源的能力 xff0c 这就能够保证大部分企业在云上所构建的业务都能够承受巨大的业务量压力 计算弹性 纵向的弹性 xff0c 即单个服务器
  • LACP 详解

    一 LACP简介 1 LACP协议简介 基于 IEEE802 3ad 标准的LACP xff08 Link Aggregation Control Protocol xff0c 链路汇聚控 制协议 xff09 是一种实现链路动态汇聚与解汇聚
  • 串口发送数据

    关于串口发送数据 自己以前呢是这样 void Usart Out Char unsigned char c uint32 t cnt while cnt USART SendData USART1 c 43 43 while USART G
  • Spring 启动分析(1)

    2019独角兽企业重金招聘Python工程师标准 gt gt gt Spring MVC 启动记录 xff08 1 xff09 1 默认的初始化就是一个DispatchServlet xff0c 这个serlet的初始化过程就是整个spri
  • ubuntu安装xfce_在Ubuntu Linux上安装Xfce(Xubuntu)

    ubuntu安装xfce Ubuntu by default includes the Gnome desktop environment but it s easy enough to install another window man
  • 缓存详解

    前言 总括 xff1a 缓存从来都是前端的一个痛点 xff0c 很多前端搞不清楚缓存到底是何物 xff0c 从而给自己创造了一些麻烦 xff0c 本文一如既往的用通俗易懂的文字和实例来讲述缓存 xff0c 希望能让您有所得 原文博客地址 x