接着上篇《<计算机与网络篇 > web缓存机制》,其细分出来四个缓存机制,而前端工程师能干预的也只有浏览器缓存这一项了,其中浏览器缓存又包括了 HTTP 缓存与应用层缓存,要讲 HTTP 缓存机制,我们首先讲讲关于浏览器缓存的一些基本知识。
既然是缓存,那肯定有储存的地方,具体上我们把 HTTP 请求的缓存空间分为这么四大类:
HTTP 的缓存空间
Service Worker
Service Worker 是独立于当前页面的一段运行在浏览器后台进程里的脚本,是一条单独的线程。它的特性将包括推送消息,背景后台同步, geofencing(地理围栏定位),拦截和处理网络请求。根据上述功能不难猜出,它可以使你的应用优先访问本地缓存资源,提供基本web应用的功能,实现web应用的离线体验(一般称之为 Offline First)。
而在 Service Worker 之前,另一个叫做 Application Cache 的 api 也可以提供离线体验。Application Cache 的主要问题在于只适合于单页 web 应用程序,对于传统的多页网站则不适合,而Service Worker 的设计则规避了这些痛点。所以本来应该还有第五个缓存空间,但因为 App Cache 的弊端,MDN 已经建议废弃并转向 Service Worker。
在 MDN 中对 Service Worker 是这样描述的,它具体的使用方法我就不在这里说明了,点击 Service Worker API 进行查看:
Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。
Service worker是一个注册在指定源和路径下的事件驱动worker。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。
Service worker运行在worker上下文,因此它不能访问DOM。相对于驱动应用的主JavaScript线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步API(如XHR和localStorage)不能在service worker中使用。
出于安全考量,Service workers只能由 HTTPS 承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。在Firefox浏览器的用户隐私模式,Service Worker不可用。
Memory Cache
Memory Cache 存储在内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如已经下载的样式、脚本、图片等。因存储在内存中,其读取速度当然比硬盘的要快上不少,但是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
我们可以拿 百度 进行测试,打开开发者进入到 NetWork 选项卡中,再次点击刷新,你可以看到有非常多的资源是写着来自 memory cache 的(在 Size 一栏中)
Disk Cache
Disk Cache 既存储在磁盘中的缓存,相对于 Memory Cache 虽读取速度较慢,但是存储容量大,不主动清理可长时间保存,同样的我们可以在 Network 中看到 disk cache。
至于它们两者是如何规定的,以及如何实现的,这些都会在下文陈述。
Push Cache
自从 2015 年 HTTP/2 标准正式发布,提供 Push Cache (服务器推送)的 API ,是一种提升首屏加载速度的技术,它允许 Web 服务器在收到浏览器的请求之前提前发送一些资源给客户端。这相当于你请求了一个 index.html,服务器则猜想你还需要请求,a.js或者a.css,在你建立请求前提前发送给你了。这个 API 貌似太少见,国内基本看不到什么文章介绍,这里也是了解一下就好。
HTTP 缓存机制
根据不同的状态,我们将 HTTP 缓存机制 分为两大类:强缓存 与 协商缓存 。
我们先来看看一个完成的 HTTP 请求,继续以百度为例(下面内容都以页面已经过首次加载后的状态下讨论)。在开发者模式下的 Network 选项卡中找到地址为 https://www.baidu.com/ 的请求项:
我们可以看到红色箭头的三个字段,分别为应答头的 Cache-Control,Expires 与 请求头的 Cache-Control(HTTP/1.1 时期添加的新字段),所以控制缓存开关的字段有两个:Expires 和 Cache-Control 。其实在 HTTP/1.0 时期还存在 Pragma 这样缓存响应头字段,不过现在基本被废弃了,其三者的优先级别为 Pragma > Cache-Control > Expires 。
Tips:在 Network 工具栏中还可以主动勾选 Disable cache 设置请求头
Cache-Control: no-cache
强制要求缓存把请求提交给原始服务器进行验证。同时当 http 请求符合强缓存策略时并不意味着客户端与服务器没有会话了,只是服务器没有返回数据而已。
那么这两个字段分别有什么值,又是如何实现强缓存的呢?
强缓存
Cache-Control
Cache-Control 分为:缓存请求指令、缓存响应指令以及扩展的三个实验指令(这里就不过多介绍了)。
- 语法说明:
请求指令 | 响应指令 |
---|---|
Cache-Control: max-age= | Cache-control: must-revalidate |
Cache-Control: max-stale[=] | Cache-control: no-cache |
Cache-Control: min-fresh= | Cache-control: no-store |
Cache-control: no-cache | Cache-control: no-transform |
Cache-control: no-store | Cache-control: public |
Cache-control: no-transform | Cache-control: private |
Cache-control: only-if-cached | Cache-control: proxy-revalidate |
空 | Cache-Control: max-age= |
空 | Cache-control: s-maxage= |
-
指令说明:
-
可缓存性
字段 | 说明 |
---|---|
public | 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容(例如,该响应没有max-age指令或Expires消息头)。 |
private | 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容。 |
no-cache | 在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证。 |
no-store | 缓存不应存储有关客户端请求或服务器响应的任何内容。 |
- 到期
字段 | 说明 |
---|---|
max-age= | 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。 |
s-maxage= | 覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。 |
max-stale[=] | 表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间。 |
min-fresh= | 表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。 |
- 重新验证和重新加载
字段 | 说明 |
---|---|
must-revalidate | 一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。 |
proxy-revalidate | 与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。 |
- 其他
字段 | 说明 |
---|---|
no-transform | 不得对资源进行转换或转变。Content-Encoding 、Content-Range 、Content-Type 等HTTP头不能由代理修改。例如,非透明代理或者如Google’s Light Mode可能对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。no-transform 指令不允许这样做。 |
only-if-cached | 表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝。 |
Tips: 其能接受多个参数,比如
Cache-Control: no-cache, no-store, must-revalidate
该指令完全关闭 http 缓存,其参数也存在着优先级no-store
为最高,s-maxage
的优先级高于max-age
。
Expires
-
Expires
即服务端发送给客户端的时间戳, 在此时候之后,响应过期,缓存失效。 -
无效的日期:比如 0 ,代表着过去的日期,即该资源已经过期。
-
如果在
Cache-Control
响应头设置了 “max-age” 或者 “s-max-age” 指令,那么Expires
头会被忽略。 -
Expires
是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。 -
示例:
Expires: Sat, 01 Dec 2029 03:17:14 GMT
协商缓存
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?同时假如强缓存所有的策略都没有命中时(比如过期等),我们又该如何处理缓存呢?这就到了协商缓存策略登场的时候了。
协商缓存主要通过 Last-Modify / If-Modify-Since 和 ETag / If-None-Match 这四个字段进行控制。
Last-Modify / If-Modify-Since
浏览器第一次请求时,应答头会包含 Last-Modify 字段,标识该资源的最后修改时间,例如 Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
。当浏览器再次请求该资源时,请求头中会包含 If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到 If-Modify-Since 后,根据资源的最后修改时间判断是否命中缓存。由于精确度比 ETag
要低,所以这是一个备用机制。
Tips:<语法> Last-Modified: < day-name >, < day > < month > < year > < hour >:< minute >:< second > GMT
但它存在着一些弊端:
-
如果本地打开或者操作过缓存文件,会造成 Last-Modified 时间被修改,导致服务端不能命中缓存导致发送相同的资源。
-
因为 Last-Modified 最小单位为秒,所以存在延迟的可能,导致缓存命中以至于未能获取最新的资源。
这时候我们就需要 ETag 的出场了。
ETag / If-None-Match
ETag 是资源的特定版本的标识符。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web服务器不需要发送完整的响应。而如果内容发生了变化,使用 ETag 有助于防止资源的同时更新相互覆盖。
它实际上就是文件的唯一标识,类似我们在用 webpack 打包生成文件的 hash 值,用来区分文件是否经过修改。
在下一次向服务器发送请求时,会将上一次返回的 Etag 值存放到请求头中的 If-None-Match 里,服务器只需要比较 If-None-Match 的值与该资源的 ETag 是否一致,就可以决定是否发送资源。
总的来说在精确度上,Etag 要优于 Last-Modified ,而性能上 Etag 要逊于 Last-Modified ,因为 Etag 需要服务器计算出一个 hash 值。而对于浏览器来说,它是遵循 Etag 优先策略的。
用户行为的影响
对于协商缓存策略,用户行为还是会显著影响 Last-Modify 与 ETag 的使用:
用户操作 | Expires | Last-Modified |
---|---|---|
地址栏回车,页面链接跳转,新开窗口,前进、后退 | 有效 | 有效 |
F5刷新 | 无效 | 有效 |
Ctrl+F5刷新(强制刷新) | 无效 | 无效 |
总结
我们用一张图来展示整个 http 的缓存机制流程:
关于浏览器缓存中另外一项:应用层缓存 将在下一篇讲述,请关注我的 慕课手记 ,以便查看!