Http 缓存

怎么说呢,这块知识说重要但是往往不是我们直接需要做的,因此很容易在实际的项目中所忽略。毕竟像 Last-Modified 以及 Etag 这些缓存的策略大部分的服务器已经帮我们实现了,拿比较熟悉的 Tomcat 来说,Tomcat 自带了 DefaultServlet,用来处理静态资源的缓存问题。但是了解了 http 缓存我们还是能够做更多的事的,比如启用 Cache-Control ,这样静态资源得到的状态码将是 200(from cache)而不是 304,可以减少 http 请求。

讲了了一些废话,那就开始慢慢介绍 http 缓存的一些基础知识!

Pragma 和 Expires

这是 http 1.0 时代,关于缓存的两个用来控制缓存的字段。

Pragma 的启用是通过添加如下的信息到 http 文件头部,来禁用客户端缓存该资源,主要是页面资源。

<meta http-equiv="Pragma" content="no-cache">

但是使用时存在一些注意点,首先只有 IE 支持这个 meta 属性,其次是往往需要放到 body 后面点我点我。总的来说这种客户端定义Pragma的形式基本没起到多少作用。而在响应头部中加入这个字段,反而能够禁用缓存生效。

有了 Pragma 来禁用缓存,自然也需要有个东西来启用缓存和定义缓存时间,对http 1.0 而言,Expires 就是做这件事的首部字段。

Expires 的值对应一个GMT(格林尼治时间),比如“Mon, 22 Jul 2002 11:12:01 GMT”来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求。

在客户端我们同样可以使用meta标签来知会IE(也仅有IE能识别)页面(同样也只对页面有效,对页面上的资源无效)缓存时间:

<meta http-equiv="expires" content="mon, 18 apr 2016 14:30:00 GMT">

如果希望在IE下页面不走缓存,希望每次刷新页面都能发新请求,那么可以把“content”里的值写为“-1”或“0”。

注意的是该方式仅仅作为知会IE缓存时间的标记,你并不能在请求或响应报文中找到Expires字段。如果是在服务端报头返回Expires字段,则在任何浏览器中都能正确设置资源缓存的时间。

另外在优先级上 Pragma 要高于 Expires,并且 Expires 存在的劣势也很明显,就是客户端和服务器时间的不一致可能导致问题。

实践:为了向下兼容 http 1.0 的标准,还是有很多网站会使用这两个字段的,正确的使用姿势也是在服务端向 response header 中设置对应的参数。

Cache-Control

针对上述的“Expires时间是相对服务器而言,无法保证和客户端时间统一”的问题,http1.1新增了 Cache-Control 来定义缓存过期时间,若报文中同时出现了 Pragma、Expires 和 Cache-Control,会以 Cache-Control 为准。

Cache-Control 是一个通用首部,有很多取值,这里主要介绍几个常用的值,其余的可以查阅 RFC 2616 文档。

值可以是public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age

各个消息中的指令含义如下:

Public指示响应可被任何缓存区缓存。
Private指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效。
no-cache指示请求或响应消息不能缓存,该选项并不是说可以设置”不缓存“,容易望文生义~
no-store用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存,完全不存下來。
max-age指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
min-fresh指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。

实践,针对页面资源,主要还是要根据页面的更新速度,如果经常变化则可以不用缓存或者设置一个较短的过期时间。腾讯首页选择的是设置 max-age=60,较短时间的缓存,而百度则是使用 private(简要介绍一下private,首先这是默认值,在地址栏回车或后退键是不会重新请求的,刷新或者第一次访问时才会请求)。而针对静态资源,一般可以设置一个较长的缓存时间,百度设置的是 30 天,部分资源会达到一年,不过在工程化的前端当中静态资源的更新是一个比较常见的场景,这会在之后的文章中有所涉及。

Last-Modified 和 Etag

服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端。

客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码即可。

至于传递标记起来的最终修改时间的请求报文首部字段一共有两个:

1、 If-Modified-Since: Last-Modified-value

示例为 If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT
该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上的一致,则直接回送304 和响应报头即可。当前各浏览器均是使用的该请求首部来向服务器传递保存的 Last-Modified 值。

2、 If-Unmodified-Since: Last-Modified-value

告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412(Precondition Failed) 状态码给客户端。

当遇到下面情况时,If-Unmodified-Since 字段会被忽略:

  1. Last-Modified值对上了(资源在服务端没有新的修改);
  2. 服务端需返回2XX和412之外的状态码;
  3. 传来的指定日期不合法

Last-Modified 说好却也不是特别好,因为如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。

为了解决上面的这个问题,http 1.1 又提出了Etag。服务器会通过某种算法,给资源计算得出一个唯一标志符(比如md5标志),比起 Last-Modified 更加能够说明文件的变化,在把资源响应给客户端的时候,会在实体首部加上“ETag: 唯一标识符”一起返回给客户端。

今天看文章讲到了Etag的劣势,在集群的情况下,对同一个资源,不用服务器给出的Etag不同。这是因为像 Apache 和 IIS,产生Etag的规则依赖于 inode。所以在集群环境下的策略是不用Etag或者配置Etag,比如把 inode 从生成规则中去掉。

客户端会保留该 ETag 字段,并在下一次请求时将其一并带过去给服务器。服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。

如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

那么客户端是如何把标记在资源上的 ETag 传去给服务器的呢?请求报文中有两个首部字段可以带上 ETag 值:

1、If-None-Match: ETag-value

示例为 If-None-Match: "56fcccc8-1699"
告诉服务端如果 ETag 没匹配上需要重发资源数据,否则直接回送304 和响应报头即可。

当前各浏览器均是使用的该请求首部来向服务器传递保存的 ETag 值。

2、If-Match: ETag-value

告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段。

If-Match 的一个应用场景是,客户端走PUT方法向服务端请求上传/更替资源,这时候可以通过 If-Match 传递资源的ETag。

需要注意的是,如果资源是走分布式服务器(比如CDN)存储的情况,需要这些服务器上计算ETag唯一值的算法保持一致,才不会导致明明同一个文件,在服务器A和服务器B上生成的ETag却不一样。

Last-Modified 和 Etag 很好,但是配合上面提到的三个东西一起使用,会有更好的效果,因为对于一些资源,完全可以使用 200(from cache),而不是重新去请求,通过 304 在做,这样可以减少大量的不必要的 http 请求。

不同用户行为的影响
用户操作 Expires/Cache-Control Last-Modified/Etag
地址栏回车 有效 有效
页面链接跳转 有效 有效
新开窗口 有效 有效
前进、后退 有效 有效
F5/按钮刷新 无效(BR重置max-age=0) 有效
Ctrl+F5刷新 无效(重置CC=no-cache) 无效(请求头丢弃该选项)
总结

在服务器为我们实现了 Last-Modified 和 Etag 的基础上,我们需要对自己的静态资源以及页面资源进行缓存的设置。对于静态资源可以设置一个较长时间的缓存,而对于页面的缓存可以根据页面的更新频次进行精确地控制,具体的策略我也需要更多的实践来进行试验。当然写了这么多很多东西还是没有涉及的,有兴趣可以再回去啃啃 http 1.1 的 RFC 文档,虽然看起来一点都不好啃。最后不得不吐槽下 ghost 博客的 markdown 支持太差了!🙄

参考资料:

1、 浅谈浏览器http的缓存机制

知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。