浓缩

cache的基本概念

我们通过使用 http cache 改进用户体验。
这种改进主要体现在两个方面:

  • 尽量不发请求来获取数据:
    对于浏览器中可重用的缓存,就不需通过网络请求来获取了;

  • 服务器返回数据时,节约贷款:
    对于不清楚是否可以重用的缓存,服务器接到请求如果发现缓存是可以重用的,只需要告诉浏览器重用缓存,而不需要把整个结果再重新发送一遍。

在浏览器端,http 缓存以 hash 表形式存放。
其中 key 一般是 url,
value 是 http response。

对于 http cache 的管理和使用,如图:

当一个可复用缓存的请求发出后,浏览器会:

  1. 查找阶段
    浏览器会在 http cache 中寻找是否有对应的缓存。如果没有,则直接向服务器请求数据。
    在接到Response后,直接跳到第4步更新缓存阶段。

  2. 保质期验证阶段
    如果找到对应缓存,会继续对这个缓存的新鲜度进行校验。没有过期的缓存,浏览器直接作为 Response 返回。如果缓存已经过期,浏览器继续向服务器请求数据,此时请求的数据将包含关于缓存的时间/状态信息
    !! 当缓存的资源没有设置缓存有效期(有效期时长/禁用缓存标志)时候 ,根据RFC7234文档4.2.2. Calculating Heuristic Freshness,浏览器可以自定义算法计算一个缓存设置

  3. 服务器校验阶段
    服务器在接到请求后,根据缓存的时间/状态信息,判断浏览器存储的缓存是否可以重用。如果可以重用,返回304;否则是200。

  4. 更新缓存阶段
    浏览器接到 response 以后,会根据 header 中设置,判断是否需要更新缓存存储。如果需要则新建/更新缓存。

而对于put/post/delete,这样的 request 成功后会删除/失效相应的缓存。

cache 的设计流程

google 提供了一份供开发者决定自己缓存配置的流程图,如图:

cache 的几个案例

首页/框架页/动态页面

这个案例来说明,response 可重用;但每次都要验证的场景。
例如:http://news.baidu.com
我们希望浏览新闻列表的用户尽可能的获得更新后新闻列表,同时节约服务器带宽。
此时,用户第一次访问新闻列表和再次访问列表复用缓存的过程,可以通过两张图来描述:

这里主要利用了

  • 服务器告知浏览器缓存设置
    http header 字段中 Cache-Control的属性 No-Cache 支持浏览器的缓存这个存储的 Response 需要先校验,以及Last-Modified:日期时间 来传递获得资源的时间戳。

  • 浏览器检验缓存,及通知服务器缓存状态
    浏览器根据缓存的 Response 中的Cache-Control:No-Cache,在保鲜期检验中不能确定缓存足够新鲜;通过将缓存的 response 上的Last-Modified值赋给 request 上 header 字段中的 Last-Modified-Since,告知服务器缓存状态。

  • 服务器检验缓存,及返回
    服务器根据request 上的Last-Modified-Since字段,获得缓存时间戳信息;比较后,如果发现缓存足够新鲜;那么将返回304,告知浏览器可复用缓存。

新闻页面的内容页

这个案例来说明,response可重用,尽可能利用缓存,但是需要验证。
我们提供的新闻内容页面可能会更改,但是更改的可能性不大。因此我们希望看新闻内容的用户,再次访问页面的时候尽可能快速看到内容。

这里主要利用了

  • 服务器告知浏览器缓存设置
    http header 字段中 Expires通知了缓存的最大保鲜期,以及Last-Modified:日期时间 来传递获得资源的时间戳。

  • 浏览器检验缓存,及通知服务器缓存状态
    浏览器根据缓存的 Response 中的Cache-Control:Expires,校验缓存足够新鲜;如果不是足够新鲜,通过将缓存的 response 上的Last-Modified值赋给 request 上 header 字段中的 Last-Modified-Since,告知服务器缓存状态。

  • 服务器检验缓存,及返回
    服务器根据request 上的Last-Modified-Since字段,获得缓存时间戳信息;比较后,如果发现缓存足够新鲜;那么将返回304,告知浏览器可复用缓存。

webpack 提供的静态资源方案

这个案例说明,response可重用,只利用缓存,不需要过期验证。
对于一般的网站而言,引用的静态文件,例如:js 文件、img 文件。
这些文件默认是不经常更改的或者定期更新,希望从缓存中读取,以提高用户体验,降低服务器压力。
但也存在着hotfix、临时修改等场景。因此对于这些静态文件的缓存,很难决策:

  • 过期时间设置的长些,则存在更新不及时的风险;
  • 设置得过短,则会导致缓存作用减小,存在浪费。

webpack 提供了一个很有意思的方案,来解决这种两难。

  1. 我们所引用的静态资源,将由 webpack 负责编译和命名;
  2. webpack 以生成的静态文件的 MD5/hash 为文件名,
    例如生成的首页脚本 index.bundle.js 当前的 hash 为05678,
    则这个index.bundle.js将被命名为05678.js,并且首页将引用05678.js 这个文件

  3. 服务器端返回给客户端05678.js,缓存有效期为永不过期

  4. 当浏览器再次需要这个文件的时候,将从本地缓存中读取。

  5. 如果index.bundle.js内容被更新,则hash 将会变换(例如:05789),更新后的首页文件引用的文件名同时发生改变。

  6. 当再次浏览首页的时候,浏览器将会请求新的05789.js,而不是从缓存中读取05678.js

浏览器刷新按钮的行为

很多时候,我们发现在chrome 浏览器中,如果我们使用刷新按钮,重载一个页面
不论这个页面的缓存设置如何,浏览器都会向服务器发送一个请求。
这是因为浏览器认定点击刷新按钮,刷新页面的行为意味着用户希望看下服务器是否存在更新。
因此,此时 chrome 浏览器会强制缓存过期:Cache-Control:max-age =0

http cache 详细剖析

哪些 http method 是可复用缓存的

  • 一般而言,Get/Head/Option/Trace等查询服务器端的 http method 的行为是可缓存的,而浏览器一般支持 Get行为;
  • 对于 Post/Put/Delete,这些 method 是不可缓存的,成功后将会删除/失效对应的缓存。

偏门知识

对于 Post 而言,协议中规定如果 Post的 Response 中带有 body,而会先失效对应的缓存,再更新缓存
但是大部分浏览器不支持这个行为,因此Post 也可以被归为不重用缓存一类的
私以为,Post 中带 Body 有点.....呵呵

Cache key的配置

http 缓存的 key 由两部分构成:target url 和 vary 字段中的 header fields。

  • target url的格式为 scheme://authority/path?query-string,大小写敏感。

    格式名称 示例 生效
    scheme http/https 生效
    authority weather.example.com:80 生效
    path /oaxaca 生效
    query ?name=daxing 生效
    fragment #nose 无效
  • vary字段:vary 字段里标记了一个 request中的 header field。
    例如:缓存中的 stored response 的 Vary:Accept-Encoding,
    且response对应的 originial request 中Accept-Encoding:gzip, deflate, sdch;
    那么新产生的 new request与 originial request具有相同的 target url,|
    Accept-Encoding也必须为gzip, deflate, sdch。

    课外小知识

    之所以采用这种分类方式,与服务器如何把一个 request 和一个 resource的representation 一致。
    如图(图片来自于w3c

    例如:http://www.ftchinese.com/video/2071 对应的resource 支持多种语言,
    那么通过 request 中的Accept-Language:en-US,en;可以获得支持英文的representation;
    而如果Accept-Language:zh-CN,可以获得支持中文的representation。

过期验证阶段对于期限的约定及算法

如图(图片来自于 google 开发者网站

http 1.1协议规定,服务器通过 response 中的 Cache-Control:max-age通知客户端,这个response的有限期;
结合 header 中的 date字段,可以算出来这个 response 的最后保鲜时间。
算法为 last-freshed-date = date+ max-age;
例如:Date:Sun, 30 Oct 2016 14:41:44 GMT;Cache-Control:max-age=600
则 last-freshed-date 为 Sun, 30 Oct 2016 14:51:44 GMT。

为了兼容 http 1.0协议,也可以通过 Expires字段来告知客户端有效期,
例如:Expires:Mon, 31 Oct 2016 06:16:14 GMT
当同时设置了 max-age 和Expires,以 max-age 为准。

http 协议支持通过一些特殊的设置来,保证每次请求都需要被服务器验证:

  • Cache-Control:no-cache
  • Cache-Control:max-age = 0
  • Pragma: no-cache(http 1.0协议,优先级比max-age低)

服务器验证对于本地缓存状态及返回值的约定

浏览器判断存储的response 已经过期后,继续向 Server 端发出数据请求。这个数据请求中会包含缓存状态的信息。
如图(图片来自于 google 开发者网站

这里通过 if-none-match 字段来传递缓存状态,值来自于存储的response head 字段中的 etag。
服务器接到请求后,通过if-none-match判定,客户端浏览器的缓存是否够新:如果浏览器缓存的 response 可被复用,那么服务器只需要返回一个状态为304的response;否则会返回一个200OK,带有 body 的 response。

除了 if-none-match/etag 可以用来传递缓存状态信息外;Last-Modified-Since/Last-Modified 也可以表示缓存的状态。

更新缓存阶段的行为约定

304更不更新本地缓存?

其他一些这里没有讨论的问题

  • 缓存的public vs private
  • 关于partial content
  • 各种 http status code
  • 浏览器告警的约定