前言, http缓存作为前端面试中经常被提问的一环,相信很多人或多或少都会有基本概念,本文基于此,使用node和chrome就浏览器对HTTP各种缓存的表现做一探究。
HTTP缓存的历史和简单介绍
作为典型的BS
架构下的程序,前端应用的一般运行方式是通过客户端各种行为,不断的从服务器上拉取资源的过程,这中间很容易出现同一个资源要被多次使用的情况,作为前端工程师,从经济角度
讲我们的期望是节省客户端跟服务器双方的流量,使得我们的应用每次的请求尽可能少,请求/响应体尽可能小; 从用户体验
角度讲我们更希望之前请求过得资源可以被保留下来,下次使用的时候直接从客户端本地拉起资源而不是又一次的从服务端开始重新请求,这样可以使得界面以一个很快的方式呈现给用户,提升用户体验。
因而HTTP缓存便应用而生了,一般来讲(以笔者的阅历来讲)HTTP缓存主要经历过两个阶段就是HTTP1.0跟HTTP1.1。
1.0 时代主要是控制expires
响应头和Last-Modified(服务端响应头)/If-Modified-Since(客户端请求头)
还有Pragma响应头(这哥们现在已经不常见了,其作用类似于1.1版本的Cache-Control: no-cache)
实现缓存控制,1.1 在此基础上又引入了Cache-Control
响应头和E-tag(服务端响应头)/If-None-Match(客户端请求头)
来实现缓存控制。下边开始逐一详解。
一. 1.0时代,expires
和Last-Modified/If-Modified-Since
1. expires
expires作为响应头,其返回的值是一个叫做http-date
的玩意,这是一个可以精确到秒的utc时间戳,js中可以使用Date.prototype.toUTCString()
得到,具体的在服务端响应头中我们放一个:
// 一天后过期
'Expires':new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString(),
现在来看看他在浏览器上的表现:
由于是首次加载, 此时并不会出现使用缓存的情况而是,向服务端请求原始资源:
现在, 我们来尝试刷新网页:
可见此时浏览器直接读取到了缓存的资源,而没有向服务端在发起请求。
2. Last-Modified/If-Modified-Since
此时我们把响应头变一下,变成:
// 一天后过期
'Last-Modified':new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString(),
然后清掉缓存后重新刷新网页,此时的响应头则变成了:
然后我们刷新网页此时的请求头是这样的:
可见之前的响应头Last-Modified
已经被请求头If-Modified-Since
带到了服务端,此时服务端便可根据If-Modified-Since
判断该资源有没有过期,如果没有过期可以直接发送一个code为304
的响应给客户端,表示NOT MODIFIED
,该资源客户端可以继续使用之前的缓存,否则则重新读取被请求的资源,并响应一个code为200的响应。
我们通过以上两个设置可以看到HTTP缓存基本表现为两种: 第一种是Expires
头这种的,下次加载时不会与服务端发生交互的,第二种是Last-Modified
这种的,下次加载时还会与服务端发生一次交互,我们把第一种叫做强缓存
,第二种则叫做协商缓存
。
1.0时代的缺陷很明显,明显就明显在有一个很重要的缺陷就是 他忽略了客户端跟服务端的时间并不是完全统一的,客户端时间的修改能直接影响客户端这边对缓存的判断,再有就是时间精度不够,以上两种方式产生的这种类型的时间戳精度只能到秒。
俗话说前人留坑,后人埋,正是因为1.0的各类缺陷,故而才会有1.1的各种新增机制,下来说一下1.1以及他是是如何解决这些问题的。
二. 1.1时代
1. cache-control
毋庸置疑,cache-control
绝对是1.1时代缓存控制的绝对主角,主要体现在两个方面,第一个是丰富的指令集,全部算起来cache-control
的响应头指令多达13种(MDN数据), 其中有控制缓存类型的,有控制缓存范围的还有控制缓存时间的;第二个则是这些指令之间还可以自由搭配,这点很好理解,比如控制作用域的就可以和其他的相互搭配,带来更为全面的缓存机制。下边简单讲几个cache-control
的响应头指令:
max-age
表示当前缓存为强缓存资源,语法为max-age=N
N是一个秒为单位的数字,代表N秒以内当前资源可以直接使用。他解决了1.0时代客户端时间修改会影响到HTTP缓存的问题,值得注意的是N代表的是当前资源从服务端生成之后的N秒内,而不是客户端接收到响应之后的N秒内,因此有其他缓存假如事先缓存这个缓存M秒,并将M以Age
写到响应头, 那客户端收到之后其计算时间是 N - M,
我们修改一下响应头做一验证:
'cache-control': 'max-age=120',
初次请求表现和上边的一致,直接看二次请求:
此时缓存是生效的,然后我们再此基础上加一个Age:
'cache-control': 'max-age=120',
初次请求的响应头:
在2秒内立即刷新:
可以看到缓存并不生效。原因就是 客户端在收到这个请求的时候实际的max-age已经变成0了因此,下次请求发起的时候,客户端便会认为此资源已过期需要重新请求。
-
s-maxage
大体等同max-age,主要为共享的缓存设置,优先级比max-age要高 -
no-cache
协商缓存的绝对掌控者, 和字面意思不大符合,他的意思是缓存可以存下来,但是每次使用之前必须要跟服务端协商协商。 -
no-store
这个才是真真的nocache,意思是任何类型的缓存都不允许。 -
private
、public
这两兄弟放一块儿讲,主要是因为他们都是控制该缓存的“作用域”的,private表示缓存只能存储到私有缓存中,比方浏览器自己的缓存。用户个人的私人信息就应该添加这个头,否则该信息会被多个用户共用,从而使用户信息遭到泄露。 public则表示会将缓存存储到共享缓存中。
2. E-tag/If-None-Match
如果说cache-control:max-age
解决了1.0时代的第一个缺陷,那etag就是为了解决第二个问题的,可以将时间戳的精度提高到毫秒级(这依赖于后端的具体实现),而且etag的内容是什么,你完全可以自己定,这就给了我们一个可以更加自由发挥的舞台,比方你完全可以返回一段hash,而后就算修改日期有变,但是文件内容没有变化,也可以认为此文件可以继续重用,就不用二次请求了。
我们在代码中写一个e-tag试试:
'etag': '12345678',
初次请求时:
而后,二次请求时:
可以看到请求头携带了If-None-Match
, 具体处理还是需要服务端做决定,同Last-Modified
。