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