16

缓存是提升用户访问速度,节省带宽,减轻服务器压力的必经之道。

下面都是针对的Http 1.1来说明,HTTP缓存都是针对浏览器客户端,其他第三方客户端不考虑。

什么是浏览器缓存

简单来说,浏览器缓存就是把一个已经请求过的Web资源(如html,图片,js)拷贝一份副本储存在浏览器中。缓存会根据进来的请求保存输出内容的副本。当下一个请求来到的时候,如果是相同的URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求(当然还有304的情况)。

缓存是根据url来处理的,只要url不一样就是新的资源。

浏览器HTTP执行机制

浏览器对于请求资源, 拥有一系列成熟的缓存策略。

只要有相应的缓存响应头(要求缓存),浏览器客户端都会对资源缓存一份,当然缓存响应头也有优先级的。

缓存模式

浏览器缓存可以分为两种模式,强缓存协商缓存

  • 强缓存(无HTTP请求,无需协商)

    直接读取本地缓存,无需跟服务端发送请求确认,http返回状态码是200(from memory cache或者from disk cache ,不同浏览器返回的信息不一致的)。

    对应的Http header有:

    • Cache-Control
    • Expires
  • 协商缓存(有HTTP请求,需协商)

    浏览器虽然发现了本地有该资源的缓存,但是不确定是否是最新的,于是想服务器询问,若服务器认为浏览器的缓存版本还可用,那么便会返回304(Not Modified) http状态码。

    对应的Http header有:

    • Last-Modified
    • ETag

流程图

流程图只考虑了200和304的状态码,其他异常状态码不考虑。
浏览器缓存流程

缓存相关的Http Header

Http Header包括请求头和响应头,http1.1会向前兼用的,会兼容http1.0的Http header,浏览器还有web服务一般都会考虑进去。

Http Header描述
Cache-Control指定缓存机制,优先级最高
Pragmahttp1.0字段,已废弃,为了兼容一般使用no-cache
Expireshttp1.0字段,指定缓存的过期时间(为了兼容)
Last-Modifiedhttp1.0字段,资源最后一次的修改时间
ETag唯一标识请求资源的字符串,会覆盖Last-Modified

Cache-Control

浏览器缓存里, Cache-Control是金字塔顶尖的规则, 它藐视一切其他设置, 只要其他设置与其抵触, 一律覆盖之.

不仅如此, 它还是一个复合规则, 包含多种值,同时在请求头和响应头都可设置(基本都可以)。

下面列举了常用的Cache-Control用法。

Cache-Control描述
no-store请求和响应都不缓存
no-cache相当于max-age:0,即资源被缓存,
但是缓存立刻过期, 同时下次访问时强制验证资源有效性
max-age缓存资源, 但是在指定时间(单位为秒)后缓存过期

Expires

http1.0中存在的字段,该字段所定义的缓存时间是相对服务器上的时间而言的,如果客户端上的时间跟服务器上的时间不一致(特别是用户修改了自己电脑的系统时间),那缓存时间可能就没啥意义了。在HTTP 1.1版开始,使用Cache-Control: max-age=秒替代,这样就不存在不一致问题了。

Last-Modified

Last-ModifiedIf-Modified-Since是一对的。

当浏览器第一次请求一个url时,服务器端的返回状态码为200,同时HTTP响应头会有一个Last-Modified标记着文件在服务器端最后被修改的时间。

浏览器第二次请求上次请求过的url时,浏览器会在HTTP请求头添加一个If-Modified-Since的标记,用来询问服务器该时间之后文件是否被修改过。

但是Last-Modified是http1.0的产物,有两个缺点:

  • 只能精确到秒级别
  • 内容完全没改变的资源文件,无法识别出来(只要修改时间变了,就算变动)。

所有就有了ETag

ETag

ETag解决了Last-Modified的缺点,http1.1的字段,优先级高于Last-Modified

理论上是ETag优先于Last-Modified,但是Nginx一会把这两个一起开启一起验证,而且Nginx ETag的计算方式把最后修改时间也算进去了。

Nginx ETag计算方式:计算页面文件的最后修改时间,将文件最后修改时间的秒级Unix时间戳转为16进制作为etag的第一部分 计算页面文件的大小,将大小字节数转为16进制作为etag的第二部分。

ETag有两种类型:

  • 强ETag

    强ETag值,表明两个资源表示的内容是逐字节相同的,并且所有其它实体字段(例如 Content-Language)也未更改。

    强ETag表示形式:"22FAA065-2664-4197-9C5E-C92EA03D0A16"

  • 弱ETag

    弱 ETag 值只用于提示资源是否相同。只有资源发生了根本改变时才会改变 ETag(修改时间和某些二进制头的差异不影响返回的文本内容那就不会改变 Etag 值)。这时,会在字段值最开始处附加 W/

    弱ETag表现形式:W/"22FAA065-2664-4197-9C5E-C92EA03D0A16"

ETag和If-None-Match是一对:

当浏览器第一次请求一个url时,服务器端的返回状态码为200,同时HTTP响应头会有一个Etag,存放着服务器端生成的一个序列值。

浏览器第二次请求上次请求过的url时,浏览器会在HTTP请求头添加一个If-None-Match的标记,用来询问服务器该文件有没有被修改。

一般网站都会把Last-ModifiedETag一起用,同时对对比,两个条件都满足了才会返回304。

Nginx实例

用实例来说明,Nginx的安装和使用请自行网上学习,例子的环境是在Mac系统运行的,然后在谷歌浏览器上访问(不同浏览器的表现会有点不一样的,而且浏览器还有快捷键直接刷新跳过缓存的)。

注意:nginx在没有设置Cache-Control:max-age=xxxexpires时,谷歌访问后,后面会变成200(from memory cache),然后就造成了文件修改后无法更新的问题(不知道什么时候过期)。这个也很好解决,只要设置过期时间为0,这样就一定不是强缓存,就不存在这些问题。

下面的例子Http Header只需要关注上面提到的相关字段。

ETag和Last-Modified例子

Nginx默认开启ETagLast-Modified

由于Nginx ETag可知,ETagLast-Modified多了文件大小比较,理论上有ETag就可以不用Last-Modified,但是为了兼容http1.0,很多web服务器都会带上Last-Modified

Etag关闭如下:

etag off;

Last-Modified关闭如下(没有找到具体关闭方式,只好在响应头中直接赋值为空):

add_header 'Last-Modified' '' always;

这些配置,可以随便设置在不同层级,http、server、location都可以。

默认的Nginx是同时开启的,所以不用处理什么。

server {
  listen       80;
  server_name  localhost;
  location / {
    #定义自己的web服务根目录
    root   /Users/Sam/www;
    #默认访问文件夹时,访问index.html或者index.htm文件
    index  index.html index.htm;
    location ~* \.(jpg|jpeg|gif|bmp|png|js|css){
      #nginx在没有设置Cache-Control:max-age=xxx和expires时,
      #谷歌访问后,后面会变成200(from memory cache),
      #然后就造成了文件修改后无法更新的问题。
      #这个很好解决,只要设置过期时间为0,这样就一定不是强缓存,就不存在这些问题
        expires 0s;
    }
  }
}

然后在根目录index.html中引入./test.js文件,然后访问index.html

首次访问,返回200,然后再次访问才会返回304。然后无论如何修改,只要文件被保存(即使内容不变),再次访问浏览器返回200,然后再次访问返回304(内容没修改)。

首次访问 Http Header(先清理缓存,才算首次访问)

#通用的header
Request URL: http://localhost/test.js
Request Method: GET
Status Code: 200 OK
Remote Address: 127.0.0.1:80
Referrer Policy: no-referrer-when-downgrade

#响应头
Cache-Control: max-age=0
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/javascript
Date: Thu, 27 Sep 2018 03:21:00 GMT
ETag: W/"5bab7b36-15"
Expires: Thu, 27 Sep 2018 03:21:00 GMT
Last-Modified: Wed, 26 Sep 2018 12:27:34 GMT
Server: nginx/1.10.1
Transfer-Encoding: chunked

#请求头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost
Referer: http://localhost/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36

第二次访问 Http Header(无修改)

#通用的header
Request URL: http://localhost/test.js
Request Method: GET
Status Code: 304 Not Modified
Remote Address: 127.0.0.1:80
Referrer Policy: no-referrer-when-downgrade

#响应头
Cache-Control: max-age=0
Connection: keep-alive
Date: Thu, 27 Sep 2018 03:23:22 GMT
ETag: "5bab7b36-15"
Expires: Thu, 27 Sep 2018 03:23:22 GMT
Last-Modified: Wed, 26 Sep 2018 12:27:34 GMT
Server: nginx/1.10.1

#请求头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost
If-Modified-Since: Wed, 26 Sep 2018 12:27:34 GMT
If-None-Match: W/"5bab7b36-15"
Referer: http://localhost/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36

第三次访问 Http Header(有修改)

#通用的header
Request URL: http://localhost/test.js
Request Method: GET
Status Code: 200 OK
Remote Address: 127.0.0.1:80
Referrer Policy: no-referrer-when-downgrade

#响应头
Cache-Control: max-age=0
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/javascript
Date: Thu, 27 Sep 2018 03:25:14 GMT
ETag: W/"5bac4d98-15"
Expires: Thu, 27 Sep 2018 03:25:14 GMT
Last-Modified: Thu, 27 Sep 2018 03:25:12 GMT
Server: nginx/1.10.1
Transfer-Encoding: chunked

#请求头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost
If-Modified-Since: Wed, 26 Sep 2018 12:27:34 GMT
If-None-Match: W/"5bab7b36-15"
Referer: http://localhost/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36

第四次访问 Http Header(无修改)

#通用的header
Request URL: http://localhost/test.js
Request Method: GET
Status Code: 304 Not Modified
Remote Address: 127.0.0.1:80
Referrer Policy: no-referrer-when-downgrade

#响应头
Cache-Control: max-age=0
Connection: keep-alive
Date: Thu, 27 Sep 2018 03:26:46 GMT
ETag: "5bac4d98-15"
Expires: Thu, 27 Sep 2018 03:26:46 GMT
Last-Modified: Thu, 27 Sep 2018 03:25:12 GMT
Server: nginx/1.10.1

#请求头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost
If-Modified-Since: Thu, 27 Sep 2018 03:25:12 GMT
If-None-Match: W/"5bac4d98-15"
Referer: http://localhost/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36

Cache-Control 和 Expires

server {
  listen       80;
  server_name  localhost;
  location / {
    #定义自己的web服务根目录
    root   /Users/Sam/www;
    #默认访问文件夹时,访问index.html或者index.htm文件
    index  index.html index.htm;
    location ~* \.(jpg|jpeg|gif|bmp|png|js|css){
      #设置30秒缓存有效期
        expires 30s;
    }
  }
}

然后在根目录index.html中引入./test.js文件,然后访问index.html

首次访问,浏览器会返回200,然后再次访问才会返回200(from memory cache),然后30秒后过期访问,如果文件没修过过,会返回304,否则返回200,继续访问如果没过期期,返回200(from memory cache)。

具体请看上面的浏览器缓存流程图。

首次访问 Http Header(先清理缓存,才算首次)

#通用的header
Request URL: http://localhost/test.js
Request Method: GET
Status Code: 200 OK
Remote Address: 127.0.0.1:80
Referrer Policy: no-referrer-when-downgrade

#响应头
Cache-Control: max-age=30
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/javascript
Date: Thu, 27 Sep 2018 03:28:03 GMT
ETag: W/"5bac4d98-15"
Expires: Thu, 27 Sep 2018 03:28:33 GMT
Last-Modified: Thu, 27 Sep 2018 03:25:12 GMT
Server: nginx/1.10.1
Transfer-Encoding: chunked

#请求头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost
Referer: http://localhost/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36

第二次访问 Http Header(不能过期,上面设置的是30秒,要在上次访问的30秒内再次访问)

#通用的header
Request URL: http://localhost/test.js
Request Method: GET
Status Code: 200 OK (from memory cache)
Remote Address: 127.0.0.1:80
Referrer Policy: no-referrer-when-downgrade

#响应头
Cache-Control: max-age=30
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/javascript
Date: Thu, 27 Sep 2018 03:28:03 GMT
ETag: W/"5bac4d98-15"
Expires: Thu, 27 Sep 2018 03:28:33 GMT
Last-Modified: Thu, 27 Sep 2018 03:25:12 GMT
Server: nginx/1.10.1
Transfer-Encoding: chunked

#请求头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost
Referer: http://localhost/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36

第三次访问 Http Header(需要过期,上面设置的是30秒,上一次访问等待30秒后访问)

#通用的header
Request URL: http://localhost/test.js
Request Method: GET
Status Code: 304 Not Modified
Remote Address: 127.0.0.1:80
Referrer Policy: no-referrer-when-downgrade

#响应头
Cache-Control: max-age=30
Connection: keep-alive
Date: Thu, 27 Sep 2018 03:32:44 GMT
ETag: "5bac4d98-15"
Expires: Thu, 27 Sep 2018 03:33:14 GMT
Last-Modified: Thu, 27 Sep 2018 03:25:12 GMT
Server: nginx/1.10.1

#请求头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost
If-Modified-Since: Thu, 27 Sep 2018 03:25:12 GMT
If-None-Match: W/"5bac4d98-15"
Referer: http://localhost/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36

第四次访问 Http Header(不能过期,上面设置的是30秒,要在上次访问的30秒内再次访问修改后的文件)

访问获取的还是旧文件,文件虽然修改了,但是浏览器直接缓存中获取,没发出请求,无法获取最新的内容。

#通用的header
Request URL: http://localhost/test.js
Request Method: GET
Status Code: 200 OK (from memory cache)
Remote Address: 127.0.0.1:80
Referrer Policy: no-referrer-when-downgrade

#响应头
Cache-Control: max-age=30
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/javascript
Date: Thu, 27 Sep 2018 03:28:03 GMT
ETag: W/"5bac4d98-15"
Expires: Thu, 27 Sep 2018 03:28:33 GMT
Last-Modified: Thu, 27 Sep 2018 03:25:12 GMT
Server: nginx/1.10.1

#请求头
Transfer-Encoding: chunked
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost
Referer: http://localhost/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36

第五次访问 Http Header(需过期,在第四次访问后等待30秒访问修改后的文件)

访问后获取到了最新文件。

#通用的header
Request URL: http://localhost/test.js
Request Method: GET
Status Code: 200 OK
Remote Address: 127.0.0.1:80
Referrer Policy: no-referrer-when-downgrade

#响应头
Cache-Control: max-age=30
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/javascript
Date: Thu, 27 Sep 2018 03:36:51 GMT
ETag: W/"5bac4fcc-15"
Expires: Thu, 27 Sep 2018 03:37:21 GMT
Last-Modified: Thu, 27 Sep 2018 03:34:36 GMT
Server: nginx/1.10.1
Transfer-Encoding: chunked

#请求头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost
If-Modified-Since: Thu, 27 Sep 2018 03:25:12 GMT
If-None-Match: W/"5bac4d98-15"
Referer: http://localhost/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36

不需要缓存的场景

缓存是会提升访问速度、节省带宽、减轻服务器压力,但是也不能滥用,否则会出现一些意想不到的问题。图片、css、js等资源文件一般都是需要缓存的,但是像接口数据等数据会变动的http请求都是不需要缓存的,否则会造成无法访问到新数据的情况。

场景

  • html文件不需要强缓存协商缓存就行了。

    这些都是特殊情况,html文件可以返回304状态,但是不要返回200(from memory cache)。html文件最好设置过期时间为0,强制跟服务器做文件修改对比(当然具体场景具体分析)。

    因为js文件和css文件是可以使用版本做控制或者随机数。

  • js代码版本迭代更新

    这个场景不是不需要缓存,而是更新了js代码版本,但是如果用户还在缓存期内,就会导致页面出错。

    这种情况就需要进行js类库版本控制,如:

    <script src="../js/jquery.js?version=1.8.9"></script>

    升级到到2.0.0时,我们需要把代码改成

    <script src="../js/jquery.js?version=2.0.0"></script>

    这样就不会访问到缓存的jquery.js。缓存是根据url来处理的,只要url不一样就是新的资源。

  • 前后端使用ajax请求接口数据

解决方式

  • Url添加随机数

    这种情况,是前端做处理。

  • 请求头添加Cache-Control: no-cache

    为了兼容http1.0,而外添加Pragma: no-cache,Cache-Control的选项有很多,具体如何选择,看场景。

    前端或者服务端都可以处理。

一些说明

做一些补充说明。

memory cache 和 disk cache

这个两个说明是谷歌浏览器的状态码附加提示语,内存缓存和磁盘缓存。其实很简单,只要页面在网页上打开访问,然后不关闭,刷新的一定是from memory cache,而页面关闭在打开一定是from disk cache。这是浏览器自身的缓存手段,磁盘缓存一定会有一份备份的,然后页面访问的时候也会在内存中缓存一份,这样刷新当前页面就不会读取硬盘,而是直接内存中获取(减少访问磁盘的次数)。

不同浏览器的一些表现差异

不同浏览器的缓存手段是一致的,但是文案展现形式,和刷新页面的方式会有差异(如火狐浏览器点击刷新按钮和回车刷新是不一样的)。

下面是针对已经强缓存,同时访问文件无修改的情况下,不同刷新方式返回的状态码总结(mac系统):

浏览器点击地址栏刷新按钮回车刷新cmd + r刷新
谷歌200(from memory cache)200(from memory cache)200(from memory cache)
火狐304200,其他地方说明已缓存304
safari200,其他地方说明已缓存200,其他地方说明已缓存200,其他地方说明已缓存

这里只说明下可能会遇到的疑惑。浏览器软件自身的处理方式,跟http缓存挂不上钩,我们也无法处理。

参考文章


Samon
1.3k 声望92 粉丝