1. 分类及作用
浏览器对 Web 页面的缓存大致可以分为对静态文件(js,css,图片等)的缓存,和对用户数据(登录信息,状态管理等)的缓存。
本文主要介绍对静态文件的缓存,第二种缓存因为一般被称为客户端存储,所以本文接下来提到的缓存特指对静态文件的缓存。
缓存,是指代理服务器或客户端本地磁盘内保存的 资源副本。利用缓存可减少对原服务器的访问,减少通信时间和通信流量。
那么浏览器的缓存机制是什么呢?
浏览器的缓存机制是基于 HTTP 协议实现的,所以我们首先要来看一下 HTTP 协议中与之相关的首部字段。
2. HTTP 中与缓存相关的首部字段
2.1 通用首部字段(请求和响应报文都能用的字段)
字段名称 | 说明 |
---|---|
Pragma | HTTP 1.0 的遗留,只有一个值,为“no-cache”,表示不读取缓存,必须向原服务器发请求 |
Cache-Control | HTTP 1.1 中用于控制缓存的字段 |
2.2 请求首部字段
字段名称 | 说明 |
---|---|
If-Modified-Since | 如果资源更新了则采取行动 |
If-Unmodified-Since | 如果资源没有更新则采取行动 |
If-None-Match | 如果 Etag 不一致则采取行动 |
If-Match | 如果 Etag 一致则采取行动 |
2.3 响应首部字段
字段名称 | 说明 |
---|---|
Etag | 资源的标识信息(只要有改动就会发生变换) |
2.4 实体首部字段
字段名称 | 说明 |
---|---|
Expires | HTTP 1.0 的遗留,值为一个时间点,表示这个时间点后该实体资源在浏览器端不可用 |
Last-Modified | 值为资源最后一次修改的时间 |
3. 浏览器缓存机制的演化
为什么首部字段里有 HTTP 1.0 的遗留字段,什么时候要用这些字段,这些字段应该怎么配合新标准的字段使用。要弄明白这些问题,我们需要先了解一下浏览器缓存机制的演化过程。
3.1 HTTP 1.0 时代的浏览器缓存
在 HTTP 1.0 时代,给客户端设定缓存可以通过 “Pragma” 和 “Expires”来实现。
3.1.1 Expires
对于 HTTP 1.0 而言,实体首部字段 Expires 用于启用缓存,并同时定义缓存失效的时间。
这个字段一般在响应首部字段中,将资源失效的日期时间告知客户端。客户端收到字段后,会将资源进行缓存。在失效日期后,如果客户端再次请求这个资源,请求会被发送到原服务器。
如果原服务器不希望客户端缓存此资源,可以将 Expires 的值设为和首部字段 Date 的值一样,或时间在它之前。
但是,如果首部字段 Cache-Control 中定义了 max-age(HTTP 1.1 标准,与 Expires 功能类似,下面会介绍), 会优先处理 max-age 指定的时间。
3.1.2 Pragma
有了 Expires 实体首部字段来开启缓存,自然要有字段来禁用缓存,Pragma 就是干这个事的。
该字段属于通用首部字段,但只用在客户端发送的请求中,而且只有一个值 “no-cache”。该字段会要求所有的代理服务器不要反悔缓存资源,必须将请求发送到源服务器。
添加这个字段主要是为了兼容 IE 和不支持 HTTP 1.1 标准的中间代理服务器,如果 IE 和中间代理服务器全都支持 HTTP 1.1,那么完全可以不用这个字段,只用我们下面会介绍到的 HTTP 1.1 标准中的 “Cache-Control: no-cache” (为保证兼容,一般两个字段都会用)。
3.2 HTTP 1.1 时代的浏览器缓存
HTTP 1.0 中定义的两个字段只能实现一些单一的功能,HTTP 1.1 添加了 Cache-Control 通用首部字段,If-Modified-Since, If-Unmodified-Since,If-Match 和 If-None-Match 这四个首部请求字段,Etag 这个响应首部字段,以及 Last-Modified 实体首部字段,极大丰富了浏览器的缓存机制。
3.2.1 Cache-Control
主要请求指令
key | value |
---|---|
no-cache | 强制向源服务器再次验证 |
no-store | 不缓存请求或响应的任何内容 |
max-age | 响应的最大有效期 |
主要响应指令
key | value |
---|---|
public | 可向任意方提供响应的缓存 |
private | 仅向指定的特定方提供缓存 |
max-age | 响应的最大有效期 |
s-maxage | 公共缓存服务器响应的最大有效期 |
must-revalidate | 可缓存,但必须再向源服务器确认 |
Expires 虽然能够使客户端对资源进行缓存,并且能指定失效时间,但是它用的是绝对时间,如果客户端的时间是自定义的,那么很有可能就会导致缓存不会建立,提前失效,或延时失效这些问题。
Cache-Control 最重要的指令就是 max-age,它的值是一个时间段,单位是秒,表示缓存在多长时间后失效。这样就解决了 Expires 采用绝对时间带来的问题。
3.2.2 校验字段和标识字段
除了 Cache-Control,其他新添加的字段可以归为校验字段和标识字段(为了理解我自己划分的)。
如果,客户端上某个资源的缓存时间过期了,但这时候其实服务器并没有更新过这个资源,如果这个资源数据量很大,客户端要求服务器再把这个东西重新发一遍过来,是否非常浪费带宽和时间呢? 校验字段和标识字段就解决了这个问题。
服务器在响应客户端对该资源的第一次请求时,返回一个资源的标识字段(包含标识信息(时间或者字符串)),然后客户端再次请求该资源时,如果通过(Cache-Control:max-age 或者 Expires)设置的缓存时间未过期,那么直接从缓存中获取资源。
如果过期了,那么就向服务器发起请求,在请求报文中包含校验字段和标识信息,服务器会比对资源当前状态跟标识信息描述的状态是否有差别,根据比对的结果以及校验字段的含义来决定是返回新资源,还是通知客户端继续使用缓存中的资源。
校验字段(请求) | 标识字段(响应) |
---|---|
If-Modified-Since/If-Unmodified-Since | Last-Modified |
If-Match/If-None-Match | Etag |
If-Modified-Since/If-Unmodified-Since,If-Match/If-None-Match都是在请求首部中,Etag 在响应首部中,Last-Modified 在响应实体中
具体的用法是:
Last-Modified
服务器在响应实体首部中添加了 Last-Modified 字段,这个字段的值是所返回资源最新的更新时间 timeA。如果客户端必须要再次向服务器请求该资源,那么可以携带 “If-Modified-Since:timeA ”字段。此时,服务器检查该资源的最近更新时间是否为timeA,如果没有更新,就通知客户端直接使用之前的缓存,如果更新了就返回更新后的资源。但是,如果在服务器上,一个资源被修改了,但实际内容并没有发生变化(比如,服务器定时更新)。这个时候就会因为 Last-Modified 时间匹配不上而返回整个实体给客户端。而且,Last-Modified 只能精确到秒,如果我们需要得到快速更新的资源(比如股票信息),有可能客户端再次请求时,服务器端该资源的最近更新时间没有变化(因为显示不出秒以内的变化)。
Etag
Etag 就很好地解决了 Last-Modified 不能解决的问题。
因为,服务器会响应一个 Etag 字段,一个表示资源唯一的字符串,一旦资源内容发生了变化,Etag 值也就会发生变化,当资源只是更新,而内容不变的时候 Etag 值不变。当客户端再次需要改资源时,请求首部字段对包含一个 If-None-Match 的字段,该字段值为之前传回的 Etag 值。如果这个 Etag 值与服务器端该资源的 Etag 值不一样,就说明了该资源在上次传回后内容发生了变更,需要传回新的资源。
4. 总结
浏览器在请求已经访问过的 URL 时,会判断是否使用缓存。首先要判断缓存是否在有效期内,判断缓存是否在有效期内,主要通过两个字段来判断:
- Expires
有效期,是服务器在响应实体中的字段,返回的是一个 GMT 时间。因为客户端可服务器端的时间可能采用的不是一个标准,所以这个方法不太可靠。 - Cache-Control 字段的 max-age 指令
也表示有效期,但是用的是时间段,单位是 s,优先级比 Expires 高,可以解决 Expires 不可靠的问题。
当缓存不在有效期内时,浏览器也不是简单地从服务器拿新资源,而是先询问服务器该资源是否有更新,有的话就返回新资源,没有的话,浏览器会继续使用该资源之前的缓存。询问的方法也有两种:
- Last-Modified 和 If-Modified-Since
服务器会在实体首部添加一个 Last-Modified 字段,值为该资源最近一次更新的时间。当缓存过期后,浏览器会把这个时间作为请求首部字段 If-Modified-Since 的值去请求服务器,询问资源是否有更新。 - Etag 和 If-None-Match
服务器会在响应首部添加一个 Etag 字段,值为一个字符串,这个字符串是对资源的编码,一旦资源内容发生变化,这个值就会变化,内容不变,值就不变。当缓存过期后,浏览器会将这个 Etag 值作为请求首部字段 If-None-Match 的值去请求服务器,询问资源内容是否有更新。
Etag 的优先级高于 Last-Modified。
但是如果两个同时使用,则要求两个都通过时,浏览器才会使用缓存,只要有一个没通过,服务器就需要返回新资源。
本文参考: