作为一个前端开发,缓存是我整天接触的概念。面试必问,工作中经常遇到。可能大家都很熟悉缓存头,但是大家有没有想过为什么HTTP缓存控制需要这样设计呢?首先,为什么会有缓存?网页中的代码和资源是从服务器下载的。如果服务器和用户的浏览器相距较远,下载过程会比较费时,打开网页也会很慢。.下次访问此网页时,您将不得不重新下载。如果资源没有变化,则不需要重新下载。所以HTTP有一个缓存功能,专门用来保存下载的资源,然后打开网页的时候直接读取缓存,自然会快很多。而且每个请求都需要服务器做相应的处理,比如解析URL、读取文件、返回响应等,服务器可以同时处理请求。有上限,也就是负载有上限。缓存减少了不必要的资源请求,使服务器能够腾出时间来处理更有意义的请求。综上所述,为了提高打开网页的速度,减轻服务器的负担,HTTP设计了缓存功能。那么HTTP是如何设计缓存功能的呢?如果让大家去设计HTTP的缓存功能,你会怎么设计呢?最容易想到的就是指定一个时间点,在这个时间之前直接使用缓存,过期之后再下载新的。HTTP1.0也是这样设计的,即Expires头,可以指定资源过期时间。在此之前,不请求服务器,直接获取上次下载的缓存内容。Expires:Wed,21Oct202107:28:00GMT但是这个设计有一个bug,不知道大家能不能猜到。首先,这个时间指的是GMT时间,也就是会换算成格林威治标准时间的时间,不存在时区问题。服务器会将本地时间转换为GMT时间。比如当前时间是xxx,你要缓存的时间是yyy,那么Expires就会设置为xxx+yyy的时间。如果浏览器时间准确的话,转换成GMT时间后应该也是xxx,所以缓存的时间是yyy。这是理想的情况。但是,如果浏览器时间不准确怎么办?转换成GMT时间后就不会是xxx了,具体缓存的时间也不会是yyy了。这就是问题。因此,过期时间不能由服务器计算,而是由浏览器自己计算。这也是HTTP1.1中修改max-age方法的原因:Cache-Control:max-age=600表示资源缓存600秒,也就是10分钟。什么时候过期让浏览器自己计算,Expires不会有问题。(这也是max-age和Expires同时存在的原因。)当然不同的资源会有不同的max-age。比如你打开b站首页,你会看到不同资源的max-age是不一样的。:比如有些库的js文件设置为31536000,也就是一年后就过期了,因为一般不会变,年的单位也没有问题。业务的js文件设置为600,表示10分钟后过期。业务代码经常变化。细心的同学可能会发现,以前是key:value形式的header,现在改成了key:k1=v1,k2=v2的形式?是的,这也是HTTP1.1的设计。他们要把缓存相关的header都聚集到一起,所以包裹一层放在Cache-Control的header中。所以叫法不一样,Expires:xxx叫做header,Cache-Control中的max-age:max-age=xxx叫做directive。嗯,改成max-age后,浏览器会在本地计算的过期时间下载新的资源。但这就够了吗?只是过期时间到了,但资源不一定会变,所以没必要重新下载同样的内容。所以需要和服务器确认内容是否真的发生了变化,如果发生变化就重新下载,否则不需要重新下载。有这样一个谈判过程。因此HTTP1.1设计了用于协商缓存的标头。说到资源过期,浏览器需要跟服务器确认是否有更新,如何判断资源过期呢?比较容易想到文件内容的哈希,或者说最后修改时间,分别叫做Etag和Last-Modified:服务器返回资源的时候会包含这两个header。当max-age时间到了,可以用etag和last-modified请求服务器,询问资源是否有更新。带有etag的标头称为If-None-Match:If-None-Match:"bfc13a64729c4290ef5b2c2730249c88ca92d82d"带有最后修改时间的标头称为If-Modified-Since:If-Modified-Since:Wed,21Oct201507:28:00GMT如果服务器判断资源发生变化,则返回200并在响应体中带上新的内容,浏览器将使用新下载的资源。如果没有变化,则返回304,响应体为空,浏览器直接读取缓存。这样多了一个协商阶段,可以避免在本地缓存过期但服务端资源没有变化的情况下重复下载。如果文件确定不变,不需要协商,怎么告诉浏览器呢?您可以使用不可变标头告诉浏览器此资源将保持不变并且不需要协商。这样即使缓存过期,也不会发送验证头(If-None-Match和If-Modified-Since):Cache-control:immutable前面我们讲了HTTP1.1,改成了Cache-control:k1=v1,k2=v2的形式,除了max-age还有哪些指令?前面我们讲了浏览器的缓存控制,但是在浏览器向服务器请求的过程中,中间可能会有很多层代理。如何控制代理服务器的缓存?浏览器中的缓存是用户自己的,称为私有缓存,而代理服务器上的缓存是任何人都可以访问的,称为公共缓存。如果资源只想缓存在浏览器中,不想缓存在代理服务器上,则设置private,否则设置public:比如这个设置表示资源可以缓存在代理服务器上,则缓存时间为1年(代理服务器的max-age设置为s-maxage设置),浏览器缓存时间为10分钟:Cache-control:public,max-age=600,s-maxage:31536000这个setting表示只有浏览器可以缓存:Cache-control:private,max-age=31536000而且缓存过期后是不是就完全不能用了?不,其实也可以使用过期的资源。有这么一条命令:Cache-control:max-stale=600stale不是新的意思。在request中包含max-stale,设置为600s,也就是说过期10分钟还是可以用的,但是就不能再用了。Cache-control:stale-while-revalidate=600也可以设置stale-while-revalidate,也就是说当和浏览器的协商还没有结束的时候,优先使用过期的缓存。Cache-control:stale-if-error=600或者设置stale-if-error,也就是说如果协商失败,优先使用过期的缓存。所以max-age的过期时间并不是完全强制的,可以允许过期一段时间。那么如果我想在缓存协商完成之前强制不使用过期的缓存怎么办?使用这个命令must-revalidate:Cache-Control:max-age=31536000,在must-revalidate的名字上可以看到它出来了,说明如果缓存失效,必须等待验证结束,中途不能使用过期的缓存。可能有同学会有疑惑。缓存不是自己设置的吗?怎么可能一个允许过期,另一个禁止过期呢?我会同时使用它们和自己一起玩吗?我肯定不会,但是CDN厂商可能会呀,如果你想禁止这种使用过期缓存的行为,你可以设置这个must-revalidate指令。最后,HTTP当然也支持禁止缓存,即:Cache-control:no-store如果设置了no-store命令,文件将不会被缓存,也就没有过期时间和后续的协商过程。如果允许缓存,但每次都需要协商,使用no-cache:Cache-control:no-store。可能有同学对no-cache和must-revalidate的区别感到困惑。来看一下:no-cache相当因为禁用了强缓存,所以每次都需要协商,而must-revalidate只是在强缓存过期后禁止使用过期缓存的过程,强制协商。至此,我们就把http的缓存设置说完了,总结一下:总结一下缓存可以加快网站的打开速度,也可以减轻服务器的压力,所以HTTP设计了缓存机制。HTTP1.0是通过Expires这个头来控制的,指定一个GMT过期时间,但是当浏览器时间不准确的时候,就会出现问题。在HTTP1.1中,更改了max-age方法来设置过期时间,让浏览器自己计算。并将所有缓存相关的控制放在Cache-control的头部,比如max-age等称为指令。缓存过期后,HTTP1.1还设计了一个协商阶段,将带有资源Etag和Last-Modied的If-None-Match和If-Modified-Since头传递给服务器询问是否过期。如果过期了,会返回200有新的内容,否则返回304让浏览器去缓存。除了max-age指令,我们还学习了这些指令:public:允许代理服务器缓存资源。s-maxage:代理服务器的资源过期时间。private:代理服务器不允许缓存资源,只有浏览器可以缓存。immutable:即使过期也不需要协商,资源不变。max-stale:如果资源过期一段时间,资源仍然可以使用。stale-while-revalidate:在验证(协商)期间,返回过期的资源。stale-if-error:如果验证(协商)出错,则返回过期资源。must-revalidate:过期后不允许使用过期资源。您必须等待谈判结束。no-store:禁用缓存和协商。no-cache:允许缓存,但每次都协商。虽然和HTTP缓存相关的说明相当多,但都是围绕max-age和过期协商来设计的。如果思路清晰,就容易记住。
