好好学习,天天向上,一流范文网欢迎您!
当前位置: >> 体会 >> 教学心得 内容页

响应消息头中,致使缓存失效的最大意思来说-control

Expires: Thu, 10 Nov 2017 08:45:11 GMT

在响应头中,设置该字段后,可以告诉浏览器在过期前无需再次请求。

但是,设置这个字段有两个缺点:

由于是绝对时间,用户可能会修改客户端本地时间,导致浏览器判断缓存失效,重新请求资源。另外,即使不考虑修改自信心,时差或错误等因素也可能导致客户端和服务端的时间不一致,导致缓存失效。

写的太复杂了。如果时间字符串中空格较多或字母较少,会造成非法属性,使设置失效。

缓存控制

知道Expires的缺点后,在HTTP/1.1中,增加了一个字段Cache-control,表示资源缓存的最大有效时间。这段时间客户端不需要向服务器发送请求

两者的区别在于前者是绝对时间,后者是相对时间。如下:

Cache-control: max-age=2592000

以下是 Cache-control 字段的一些常用值:(完整列表参见 MDN)

这些值可以混合使用,例如Cache-control:public,max-age=2592000。组合使用时,它们的优先级如下:(图片来自developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn)

这里有一个问题:max-age=0是不是相当于no-cache?从规范的字面意思来看,max-age expiration should (SHOULD) re-validate,no-cache must (MUST) re-validate。但实际情况以浏览器实现为准,两者的行为在大多数情况下还是一样的。(如果max-age=0,must-revalidate相当于no-cache)

顺便说一句,在HTTP/1.1之前,如果要使用no-cache,一般都是使用Pragma字段,比如Pragma: no-cache(这也是Pragma字段的唯一取值)。但该字段只是浏览器的一种约定俗成的实现方式,没有确切的规范,缺乏可靠性。它应该只是作为一个兼容字段出现,在当前的网络环境下它的用处已经很小了。

综上所述,从HTTP/1.1开始,Expires逐渐被Cache-control取代。Cache-control是一个相对时间。即使客户端时间发生变化,相对时间也不会随之改变,从而保持服务端和客户端时间的一致性。而且Cache-control的可配置性比较强大。

Cache-control 的优先级高于Expires。为了兼容HTTP/1.0和HTTP/1.1,我们会在实际项目中设置这两个字段。

比较缓存(也称为协商缓存)

当缓存被强制作废时(超过指定时间),需要使用对比缓存,由服务器决定缓存内容是否作废。

按照流程,浏览器首先请求缓存数据库,返回一个缓存标识。然后浏览器使用这个标志与服务器通信。如果缓存没有失效,会返回HTTP状态码304表示继续使用,所以客户端继续使用缓存;如果无效,它将返回新的数据和缓存规则。浏览器响应数据后,会将规则写入缓存数据库。

对比缓存在请求数上和无缓存是一致的,但是如果是304,只是返回一个状态码,并没有实际的文件内容,所以节省响应体体积是它的优化点。它的优化涵盖了文章开头提到的请求数据的三个步骤中的最后一个:“响应”。通过减小响应体大小来减少网络传输时间。所以与强制缓存相比改进很小缓存文件在哪里,但比没有缓存好。

对比缓存可以和强制缓存一起使用,作为强制缓存失败后的备份方案。在实际项目中,它们确实经常一起出现。

比较缓存有 2 组字段(不是两个):

Last-Modified & If-Modified-Since 服务器通过Last-Modified字段通知客户端资源最后修改时间,例如

最后修改时间:2018 年 11 月 10 日星期一 09:10:11 GMT

浏览器将此值与缓存数据库中的内容一起记录下来。

下次浏览器再请求同一个资源时,会从自己的缓存中找到“不确定是否过期”的缓存。因此,将最后一个Last-Modified值写入请求头中请求头的If-Modified-Since字段

tp5.0文件缓存_手机爱奇艺缓存qsv文件_缓存文件在哪里

服务器将 If-Modified-Since 的值与 Last-Modified 字段进行比较。如果相等,则表示不修改,响应304;否则表示已修改,响应200状态码并返回数据。

但他仍然有一些缺陷:

Etag 和 If-None-Match

为了解决以上问题,出现了一组新的字段Etag和If-None-Match

Etag存储文件的特殊标识(一般由hash生成),服务端存储文件的Etag字段。后续过程与Last-Modified相同,只是将Last-Modified字段及其代表的更新时间改为Etag字段及其代表的文件哈希,将If-Modified-Since改为If-None -匹配。服务器也进行比较,命中返回 304,未命中返回新资源和 200。

Etag 优先于 Last-Modified

缓存摘要

当浏览器想要请求资源时

调用Service Worker的fetch事件响应,查看内存缓存,查看磁盘缓存。这里再细分一下:如果有强制缓存,而且还没有过期,那么就使用强制缓存,不去请求服务器。此时状态码都是200,如果有强制缓存但是已经过期了,使用比较缓存判断比较后是304还是200。

发送网络请求,等待网络响应,将响应内容存入磁盘缓存(如果HTTP头信息配置可以存入),将响应内容的引用存入内存缓存(忽略HTTP头信息配置) ,并将响应内容存储在Service Worker的Cache Storage中(如果Service Worker的脚本调用cache.put())一些情况

光看原理很无聊。我们编写一些简单的网页和用例来深入理解以上原理。

1.内存缓存&磁盘缓存

我们写一个简单的index.html,然后引用三个资源,分别是index.js、index.css和mashroom.jpg。

我们为这三个资源设置了Cache-control: max-age=86400,也就是强制缓存24小时。以下屏幕截图均使用 Chrome 的隐身模式截取。

1. 第一次请求

不出所料,所有的网络请求都被接受了,因为还没有缓存。

2.再次请求(F5)

对于第二个请求,三个请求都来自内存缓存。因为我们没有关闭TAB,所以浏览器将缓存的应用添加到了内存缓存中。(耗时0ms,即1ms以内)

3.关闭TAB,打开新的TAB,重新请求

因为关闭了TAB,所以内存缓存也清空了。但是磁盘缓存是持久化的,所以所有的资源都来自于磁盘缓存。(大约需要 3ms,因为文件有点小)

并且比较2和3,很明显内存缓存比磁盘缓存快很多。

2. 无缓存和无存储

我们将一些代码放在 index.html 中以实现两个目标:

缓存文件在哪里_tp5.0文件缓存_手机爱奇艺缓存qsv文件


<link rel="stylesheet" href="/static/index.css">
<link rel="stylesheet" href="/static/index.css">
<script src="/static/index.js">script>
<script src="/static/index.js">script>
<img src="/static/mashroom.jpg">
<img src="/static/mashroom.jpg">

<script>
    setTimeout(function () {
        let img = document.createElement('img')
        img.src = '/static/mashroom.jpg'
        document.body.appendChild(img)
    }, 1000)
script>

1、当服务器响应设置为Cache-Control:no-cache时,我们发现打开页面后,三个资源都只被请求了一次。

这说明了两个问题:

一般来说,如上文所述,no-cache语义上是指下一次请求不直接使用缓存但需要进行比较,不限制本次请求。因此,浏览器在处理当前页面时可以安全地使用缓存。

2. 当服务器响应设置为Cache-Control: no-store 时,情况发生了变化,三个资源都被请求了两次。又因为对图片多了1次异步请求,一共3次。(红框内的都是异步请求)

这也说明:

3. Service Worker & 内存(磁盘)缓存

缓存文件在哪里_tp5.0文件缓存_手机爱奇艺缓存qsv文件

让我们也尝试包括 Service Worker。我们写一个serviceWorker.js,写入如下内容:(主要是预缓存3个资源,并在实际请求时匹配缓存返回)

// serviceWorker.js
self.addEventListener('install', e => {
  // 当确定要访问某些资源时,提前请求并添加到缓存中。
  // 这个模式叫做“预缓存”
  e.waitUntil(
    caches.open('service-worker-test-precache').then(cache => {
      return cache.addAll(['/static/index.js', '/static/index.css', '/static/mashroom.jpg'])
    })
  )
})
self.addEventListener('fetch', e => {
  // 缓存中能找到就返回,找不到就网络请求,之后再写入缓存并返回。
  // 这个称为 CacheFirst 的缓存策略。
  return e.respondWith(
    caches.open('service-worker-test-precache').then(cache => {
      return cache.match(e.request).then(matchedResponse => {
        return matchedResponse || fetch(e.request).then(fetchedResponse => {
          cache.put(e.request, fetchedResponse.clone())
          return fetchedResponse
        })
      })
    })
  )
})

缓存文件在哪里_手机爱奇艺缓存qsv文件_tp5.0文件缓存

注册SW的代码这里不再赘述。此外,我们还为服务器设置Cache-Control:max-age=86400,开启磁盘缓存。我们的目的是研究两者的优先级。

1、当我们第一次访问时,会看到浏览器(准确的说是Service Worker)除了正常的请求外,还额外发出了3次请求。这来自预缓存代码。

2、第二次访问(无论是关闭TAB再打开,还是直接按F5刷新),可以看到所有的请求都标记为来自SerciceWorker。

from ServiceWorker 仅表示请求已通过 Service Worker。至于是命中缓存还是继续fetch()方法,光看这条记录其实是不可能知道的。所以,还得结合后面的网络记录来看。因为之后没有再有请求,所以判断缓存命中。

从服务器日志也可以清楚的看到,这三个资源都没有被重新请求,也就是命中了Service Worker的内部缓存。

3、如果修改serviceWorker.js的fetch事件监听代码,改成如下:

// 这个也叫做 NetworkOnly 的缓存策略。 
self.addEventListener('fetch', e => {
   return e.respondWith(fetch(e.request)) 
})

可以发现,后续访问时的效果与修改前完全一样。(也就是Network只有少数几个请求是从ServiceWorker打出来的,服务端并没有打印这3个资源的访问日志)

显然,Service Worker层并没有去读取自己的缓存,而是直接使用fetch()去请求。所以这个时候,其实就是Cache-Control的设置:max-age=86400,也就是内存/磁盘缓存。但是只有浏览器知道它是内存还是磁盘,因为它没有明确地告诉我们。(个人猜测是内存,因为无论是耗时0ms还是从不关闭TAB,更像是内存缓存)

浏览器行为

所谓浏览器行为是指当用户操作浏览器时会触发什么样的缓存策略。主要有3种类型:

缓存应用模式

了解了缓存的原理之后,我们可能更关心的是如何在实际项目中使用它们,从而更好地让用户缩短加载时间,节省流量。这里有几个常用的模式供大家参考

模式 1:不经常更改的资源

Cache-Control: max-age=31536000

通常在处理这类资源资源时,为其Cache-Control配置一个较大的max-age=31536000(一年),这样以后浏览器再请求相同的URL时,就会命中强制缓存。为了解决更新问题,需要在文件名(或路径)中加入hash、版本号等动态字符,然后改变动态字符,达到改变引用URL的目的缓存文件在哪里,从而使之前的失效mandatory cache(其实并不是立即Failed,只是不再使用)。

网上提供的类库(如jquery-3.3.1.min.js、lodash.min.js等)都采用这种模式。如果在配置中加入public,CDN也可以缓存,效果很突出。

这种模式的一个变体是在 referrer URL 之后添加参数(如 ?v=xxx 或 ?_=xxx),这样就不必在文件名或路径中包含动态参数,满足一些完美主义者的喜好. 每次构建项目时,更新附加参数(例如设置为构建的当前时间)可以保证浏览器在每次构建后总能请求到最新的内容。

缓存文件在哪里_tp5.0文件缓存_手机爱奇艺缓存qsv文件

特别注意:在处理Service Worker时,需要格外小心sw-register.js(注册Service Worker)和serviceWorker.js(Service Worker本身)。如果这两个文件也使用这种模式,你必须想好以后可能的更新和对策。

模式 2:频繁更改资源

Cache-Control: no-cache

这里的资源不仅仅是指静态资源,还可能是网络资源,比如博文。这类资源的特点是 URL 不能改变,但内容可以(而且经常)改变。我们可以设置Cache-Control:no-cache,强制浏览器每次请求都要找服务器验证资源是否有效。

既然提到验证,那么ETag或Last-Modified就必须出现。这些字段是由专门处理静态资源的常用类库(如koa-static)自动添加的,开发者无需过多关心。

就像上面提到的协商缓存,在这种模式下,节省的不是请求的数量,而是请求体的大小。因此其优化效果不如模式1显着。

方式三:非常危险的方式一和方式二的组合(反例)

Cache-Control: max-age=600, must-revalidate

不知道有没有开发者从模式一和模式二中得到一些启发:在模式二中,设置了no-cache,相当于max-age=0, must-revalidate。我的应用程序不是那么时间敏感,但我不想做太长的强制缓存。我可以配置折衷设置,例如 max-age=600、must-revalidate 吗?

从表面上看,这看起来不错:资源可以缓存 10 分钟,10 分钟内从缓存中读取,10 分钟后与服务器验证。它是两种模式的结合,但在实际线路中存在风险隐患。上文提到,浏览器的缓存有自动清理机制,开发者无法控制。

例如:当我们有 3 个资源时:index.html、index.js、index.css。我们对这三个进行上面的配置后,假设在一次访问中,index.js已经被缓存清空,不存在了,但是index.html和index.css仍然存在于缓存中。这时候浏览器会向服务器请求新的index.js,然后用旧的index.html和index.css显示给用户。这样做的风险是显而易见的:不同版本的资源组合在一起,很可能会出现错误报告

除了自动清理带来的问题,不同资源的不同请求时间也会导致问题。比如A页面请求A.js和all.css,B页面请求B.js和all.css。如果我们按照A->B的顺序访问页面,必然会导致all.css的缓存时间早于B.js。那么以后访问B页面时也存在资源版本不匹配的隐患。

一位开发者朋友(wd2010)在评论区提出了一个很好的问题:

如果我不使用must-revalidate,只使用Cache-Control: max-age=600,浏览器缓存的自动清理机制不会被执行吗?如果执行了浏览器缓存的自动清理机制,后续index.js被清理造成的情况是一样的!

这个问题涉及到几点,我补充说明一下:

结论是没有区别。列出max-age后,是否列出must-revalidate效果相同,浏览器会在超过max-age后检查,验证缓存是否可用。

HTTP规范中只说明了must-revalidate的功能,并没有说明在没有列出must-revalidate的情况下浏览器应该如何解决缓存过期的问题,所以这其实是浏览器在实现时的自主决定. (可能有少数浏览器会在源站无法访问时选择继续使用过期的缓存,但这取决于浏览器本身)

是的。问题的发生与是否列出'must-revalidate'无关,仍然会出现JS CSS等文件版本不匹配的问题。因此,当一个常规网站需要在不同的页面使用不同的JS CSS文件时,如果想使用max-age来加强缓存,时间不要设置的太短。

既然存在版本不匹配的问题,那么有两种方法可以避免这个问题。

首先,整个站点使用相同的 JS 和 CSS,合并文件。这个比较适合小型站点,否则可能过于冗余而影响性能。(不过由于浏览器自带的清理策略,可能还是会被清理掉,还是有隐患)

第二,资源是独立使用的,不需要和其他文件结合才有效。例如,RSS 就属于这一类。

后记

这篇文章确实有点长,但是涵盖了大部分前端缓存,包括HTTP协议中的缓存,Service Worker,以及Chrome浏览器的一些优化(Memory Cache)。希望开发者善用缓存,因为它往往是最容易想到的,也是最大的性能优化策略。

参考文章

A Tale of Four Caches(不过这篇文章将Service Worker的优先级排在了内存缓存和磁盘缓存之间,这和我的实验结果不一致,我怀疑可能是这两年chrome policy的修改? )

缓存最佳实践和 max-age 陷阱