我们每时每刻使用的互联网, 移动手机 APK, 都是由各种各样的资源拼成的 html(JS,CSS)页面. 这些资源绝大多数是静态资源, 他们大多数都是不需要实时更新的. 比如图片, CSS 样式, JS 库, 这些静态资源构成了互联网的框架. 比如我们用浏览器追踪 (F12-> 网络)某知名互联网网站首页:
这些资源文件都很小, 但是由于往往需要每次刷新页面时候都会重新下载, 如果有什么方法可以减少对这些图像, 样式等固定文件的下载, 只获取必须 API 实时的数据然后渲染页面则用户访问肯定会更快更流畅. 其实上 HTTP 协议本身就提供一个强大的机制来解决这个问题, 这就是今天虫虫要给大家介绍的 HTTP Cache 缓存. 作为一个 web 开发者必须熟练掌握 HTTP 的缓存机制, 它可以帮我们节省大量的带宽, 服务器硬件, 极大的优化我们网站和 App 的性能改善用户体验.
缓存基础知识
我们首先从概述缓存基本概念讲起. 如果我们知道一些资源 (图片, CSS 样式文件等) 在一段时间内会不改变, 则可以用缓存保存这些资源. 在设置的时间内, 资源被认为是新鲜(fresh), 过了这段时间后设置它的状态为过期(stale).
缓存允许客户端 (例如浏览器) 尽可能长时间地保留住资源, 然后过期后丢弃它再从服务器获取新版本. 为了使缓存机制能生效, 需要一种方法来发送资源的过期时间.
为了解决这个问题, HTTP 提供了两种主要方式. 下面我们首先讨论第一种方法 .
缓存过期 Expires 和 HTTP/1.0 缓存的源起
缓存过期 Expires
Expires 是在 HTTP/1.0 协议中引入的, 它与 Pragma,Last-Modified 和 If-Modified-Since 共同构成了 HTTP 缓存体系. Expires 也是我们可以使用的最简单的 HTTP 缓存标识头, 表示给定资源过期的时间. 我们来看一个例子:
上图中这个 logo 的过期时间为 "Expires: wed, 15 May 2019 88:07:42 GMT". 如果超过 Expires 指定的日期, 浏览器就会尝试重新获这个资源取. 到期之前浏览器都会缓存这个资源, 刷新页面时候并不会再从服务下载.
使用 Last-Modified 和 If-Modified-Since 验证
要做到完美的缓存, 就要做到仅仅在确定资源更新时候才重新下载它. 实现这个目标的一种方法是允许浏览器根据这个资源去询问服务端. 浏览器怎么确定目前资源的更新版本呢? 有一个 HTTP 请求 If-Modified-Since 标识.
假设我们在该资源过期日期 5 月 16 日请求该资源, 客户端浏览器会发起请求:
请求头总包含 "If-Modified-Since", 它表示浏览器已经下载过服务器 18 年 12 月 25 日修改过的版本. 收到该请求后, 服务器会判断, 这个日期之后, 图像是否已经更新, 如果是, 则服务器会响应下载新的图像下载. 否则响应 "304 Not Modified"
收到此这个响应, 浏览器就从浏览器缓存中读取资源, 不再从服务器下载. 通过使用 Last-Modified 和 If-Modified-Since 可以确保客户端不会重复下载资源, 也可以确保服务器端有变化时候, 客户端可以及时更新到最新的资源.
用 Pragma 更新缓存
虽然 HTTP/1.0 没方法让服务器告诉客户端不缓存特定资源, 但通过客户端请求可以设置 HTTP 请求头, 不为该资源请求缓存, 这个头方法叫 Pragma:
Firefox 的调试工具中, 有个 "禁用缓存" 的复选框, 选择后, HTTP 请求就会自动在请求头中增加 "Cache-Control: no-cache"
该请求就不会使用缓存直接从服务器请求该资源, 如下图, HTTP 状态码返回为 200 而非之前的 304.
Pragma 最初设计可能为了抓取标题所用. 后续的 HTTP/1.1 为兼容也严格支持该选项.
HTTP/1.1 和 cache-control
为了克服 Expires 的局限性, HTTP/1.1 中引入了 cache-control, 极大地增强了开发人员管理缓存资源的灵活性. cache-control 不严格依赖日期, 而通过一些指令来完成对缓存的管理.
输入 max-age 指令
我们可以将 max-age 指令看成是对 Expires 的简单替代方法. 比如上面对应于 5 月 15 号, 一个月过期的日期(259200s), 对应的 cache-control 头进行响应:
注意, max-age 是对应于请求的时间的, 所以在缓存生成时开始计算. 单位为持续的秒数, 由于不用考虑时区等因素, 这种方法更加简单准确.
max-age 指令可以支持的最多一年的持久时间, 可以满足绝大多数情况的需求.
使用 Etag 和 If-None-Match 更新缓存
HTTP/1.1 还引入一种新的 Etag 缓存更新策略, 用来补充 If-Modified-Since. 我们将实体标记视为服务器唯一标识 Etag, 响应标头中使用带有字母数字 ID 的资源版本表示方法:
客户端下次请求时候, 会使用 "If-None-Match" 头通知服务器端目前缓存的资源版本的 ID 特定版本的资源:
如果资源的最新版本与上面的实体标签 ID"5c2209c2-14d05" 不匹配, 则服务器会响应新版本的 ID. 否则响应 "304 Not Modified".
为了防止 ID 名重名, 一般会使用散列 (比如 MD5) 来表示正 Etag 的 ID, 通过对资源进行计算散列可以保证文件变更和验证, 也能防止资源被篡改.
通过私有和公共方式确保缓存隐私
上面我们讨论了, 基于浏览器的本地 HTTP 缓存, 他在第一次请求时候在本地缓存资源. 现实中, 我们请求的资源在被下载到本地之前通过一个或多个缓存或 "共享" 缓存(CDN). 这些缓存或者代理由 ISP 供应商或者或服务商 IT 部门提供. 在 HTTP 访问中, 各级中间缓存都会缓存并且浏览这些资源.
为了解决这个问题, HTTP/1.1 引入了私有缓存和公共缓存控制指令. 尽管这些指令还不十分完善, 但是, 我们可以使用它来设置, 某些资源不会被在公共代理中被缓存.
如果多个人共享电脑, 他们则可以共享一个缓存. 如果资源指定了私有缓存指令, 那么浏览器只会让请求他用户可以使用它.
使用 no-store 和 no-cache 限制缓存
HTTP/1.1 纠正了 HTTP/1.0 的 Pragma 头的不足, 并为 Web 开发人员提供了一种可以完全禁用缓存的方法. 第一个指令 no-cache 强制缓存在重用之前重新验证. 与 must-revalidate 不同, no-cache 强制浏览器在必须重新验证.
第二个指令, no-store 表示资源在任何情况下都不会被缓存.
限制特定请求的缓存
如果我们想要申请至少在一定时间内刷新的资源, 该怎么办? 也没有问题! 缓存控制不仅仅可以通过服务器控制客户端的缓存, 相应地客户端也可以用来指示对某些缓存的限制.
max-age,no-cache 和 no-store 指令都支持在客户端请求头中使用. 但是注意具体的意义可能是相反的. 例如, 在请求中指定 max-age 标头会通知代理服务器它们不能使用任何早于该标头指定的持续时间的缓存响应.
除上面的三个指令外, 我们还可以使用四个仅在请求头中使用的缓存控制指令.
第一个是 min-fres: 它允许客户请求在设定时间秒数内会更新的资源.
max-stale 指令通知缓存服务器, 客户端愿意接受过期的资源, 且过期不超过设定秒数的缓存.
no-transform 指令通知缓存服务器客户端不希望请求任何版已经被修改该过的资源的缓存.
最后一个指令 only-if-cached 通知缓存服务器客户端只需要一个缓存的响应, 且不需要直接请求服务器获得缓存状态. 如果缓存无法满足请求, 则应返回 504 网关超时响应.
Vary 头和服务器协商的响应
我们最后要说明的浏览器如何识别缓存资源, 以及服务器协商怎么进行.
浏览器缓存实际上只查看 URL 和方法, 由于几乎所有可缓存的请求都是 GET 请求, 所以浏览器通过 URL 就能识别资源. 客户端服务器用于协商的 HTTP 头标识, 服务器通过 Vary 标头传送给客户端. 例如, 客户端发出以下请求:
Accept-Encoding 头表示在服务器端支持的情况下允 Web 服务器采用 gzip 对响应的资源进行压缩传输. 服务器需要响应协商请求头时候会使用 Vary 标识头, 它会将其附加到其响应头的 Vary 标头中, 如下图所示:
这样, 对资源缓存时候不仅应该使用 URL 的值来缓存响应, 而且加上使用请求头的 Accept-Encoding 值来进一步限定缓存的键. 因此使用不同 Accept-Encoding 标识头的请求(例如 deflate), 则其缓存就不用 gzip.
总结
缓存是增强 Web 服务和应用 App 性能的一种非常强大的方法, 本文旨在指导 Web 开发者和相关码农了解 HTTP 缓存, 并将其作为一们必须的工具来学习. 如果你想需要更深入的学习, 可以参考 MDN 的文档学习.
来源: http://stor.51cto.com/art/201904/595051.htm