农村的师傅的博客

一个迫于生计,无法放飞自我,导致喜欢上了前端开发,并即将成长为强者(指头发)的程序猿。

0%

浏览器缓存

通常我们在页面访问一个静态资源(通常是get请求)比如js、css、图片等,在获取到这些静态资源后,浏览器可以根据一定的规则将资源进行缓存,以便下次再请求相同的资源时,可以直接使用缓存,而无需重复加载。

资源缓存场景

  • 对于一个资源,如果每次请求都需要从服务器中去获取,尤其是这个资源没有改变时,极大浪费时间和带宽。
  • 那么如果浏览器将资源缓存到自己的内存或者磁盘,且它知道某个资源没有改变,那么就可以直接使用缓存了
  • 如果浏览器发现资源过期或者资源改变了,那么再从服务器中去获取新的资源,并缓存最新的资源。

我们可以使用http相关的请求头来启用浏览器对加载的某个资源的缓存控制。通常我们称之为强缓存和协商缓存。

缓存控制

即强缓存

这里涉及到两个请求头,expires和cache-control,他们都可以控制浏览器如何处理资源的缓存。其中expires是绝对值,表示某个资源何时过期,以expires控制的资源缓存,只要浏览器判断改资源缓存没有过期,即超过expires设定的时间,则浏览器在请求该资源时就会一直使用缓存,而不会去服务器下载。该请求头虽然提供对资源缓存的控制,但也存在一些问题,因为这并没有考虑到缓存在服务端过期时,该如何处理,只能等到过期时间到了,才会去访问新的资源,这个时间太不确定了。而且,这个时间还是以本地时间为准的,如果本地时间被修改了,那么缓存的有效时间就不准确了。

为了解决上面的问题,则新增加了一个cache-control的header头用来控制缓存(cache-control的优先级比expires高)。他通常有以下几个值:

  • Cache-Control: no-store:不对资源进行缓存,即不使用缓存。永远都从服务器中获取最新的资源。
  • Cache-Control: no-cache:浏览器会对资源进行缓存,但是在使用缓存之前,强制要求提交请求到服务器进行验证缓存是否有效(或者max-age=0)参考下文的缓存验证
  • Cache-Control: max-age=31536000:浏览器会对资源进行缓存,且该值设置的含义为:相对于请求的时间,在31536000秒后,缓存失效,失效之前可以一直使用,无需发送请求到服务器,此时资源请求直接返回200。因为这种失效只是一种时间上的设定,不代表其资源真的失效,所以在其超出设定的时间后,浏览器可以提交请求到服务器进行验证缓存还是否有效(即会走缓存验证这一步)
  • Cache-control: public:可以被公有服务缓存,比如cdn
  • Cache-control: private:只能被单个用户缓存,比如本地缓存
  • Cache-control: public,max-age=31536000:你还可以写多个值

启发式缓存算法:如果你的http请求相应中没有任何关于缓存控制的header(expires和cache-control),那么浏览器会根据一个算法算出资源合理的缓存时间,大多数浏览器使用:(当前时间- last modified时间) *10%。比方说你请求了一个一年都没有修改的文件,那么他会储存36.5天。

缓存验证

即协商缓存

上面我们知道,如果Cache-Control被设置为了no-cache或者到达了max-age设置的过期时间max-age=0, must-revalidate,那么客户端需要发送请求到服务器去验证这个资源是否有修改,即验证缓存是否还有效。

  • 如果缓存仍然有效,则返回304,表示资源可以使用本地的缓存,不需要进行资源的重新下载
  • 而如果缓存失效,则会返回200,并将最新的资源直接返回,并设置新的缓存控制header

那么,服务器如何验证某个资源是否已经被更新,缓存失效了呢?如果要验证一个文件是否被更改,有哪些方式?

一个方式是使用更新时间,如果更新时间不变,那么说明这个文件没有被修改过,但是这个方式不够精确,因为更新时间很容易在文件内容不变的情况下,被更新。比如文件被一个同样内容的文件覆盖了,虽然更新时间变了,但是文件内容没有变。这就有问题了。

那么我们可以这么想,文件是否被更新,主要看文件内容是否和之前一样,如果文件内容和之前有变化,说明文件被更新了,通常,我们不会真的根据文件内容去比较,因为不管从校验效率上还是http请求校验的代价都太大了,所以通常我们会通过哈希算法为资源生成一个数据指纹,然后只需要对比这个指纹是否有变化即可确定这个资源是否有更新。

根据以上内容,这里就涉及到两对header头,一个是用作资源修改验证的,一个是用作数据指纹验证的,他们分别是:

  • ETag和 If-None-Match:根据数据指纹进行资源校验(其优先级比Last-Modified高,因为其校验的结果更加准确)
  • Last-Modified和If-Modified-Since:根据资源修改时间进行资源校验

Etag和Last-Modified在访问资源的响应头中被设置,用来标识这个资源的数据指纹和最新修改的时间。

If-None-Match和If-Modified-Since在缓存校验中,被放到http的请求头中发送给服务器,服务器只要发现请求如果存在这两个条件式请求header(或者其中任意一个),则根据这两个值来确定这个资源是否被更新。

这两个条件首部都只能用在 GET 或 HEAD 请求中使用。而且我感觉如果没有使用Cache-Control这个header,而是只设置了expires这个header,那么如果资源过期了,浏览器仍然可以利用ETag和Last-Modified去向服务器进行缓存检查。

以下是资源缓存和缓存校验的大致步骤:

  • 当你第一次请求一个资源时,没有任何缓存控制,会直接命中到服务器上的资源,此时资源被下载到浏览器本地,正常来说,这个请求的响应头里会有缓存控制相关的header,比如Cache-Control、ETag值和Last-Modified值,假设,这里设置的是Cache-Control:no-cache吧。
  • 当你第二次请求同样的资源时,浏览器发现你的缓存控制使用的是Cache-Control:no-cache,即需要发送请求到服务器中进行缓存校验,然后才决定是否使用缓存。
  • 浏览器发送该资源的请求,并且,请求中携带上If-None-Match: 上次请求响应中的ETag的值If-Modified-Since: 上次请求响应中的Last-Modified的值 这两个条件式请求头
  • 服务器接收到请求,发现有If-None-Match和If-Modified-Since这两个条件式请求首部,服务器优先判断If-None-Match,即通过ETag来对资源进行校验,如果服务器不支持或者没有If-None-Match条件请求首部,则会使用If-Modified-Since请求首部来通过资源修改时间来对资源进行校验。(具体校验的条件,可参考上面的MDN文档)
  • 如果服务器校验发现资源未更改,则直接返回304,且没有响应体,此时浏览器就可以直接使用该资源的缓存
  • 如果服务器校验发现资源被修改了,则会返回200状态码且将最新的资源也返回回去,并且会携带最新的ETag和Last-Modified的值。
  • 浏览器根据返回状态码来确定要做的操作:如果是200,则接收响应体中的资源并更新现有缓存,如果是304,则直接使用缓存。

资源返回状态说明

  • 200:from disk cache(磁盘缓存)和from memory cache(内存缓存) 表示直接使用缓存,没有缓存校验
  • 200:正常从网络下载,则表明第一次下载或者协商缓存时,缓存失效的情况
  • 304:(Not Modified)从服务器中进行了缓存校验,且表示没有修改,可以直接使用缓存。

设置缓存控制的方法

通常来说,在服务端,比如nginx那里针对不同的资源设置相应的响应头header即可。如果是cdn服务,那么cdn通常都会设置好相关的缓存header。

如何选择强缓存和协商缓存以及不缓存

缓存的使用,自然需要根据业务需求来使用,不过,在目前的前端开发中,还是有一些常见的缓存设置场景的:

现代前端项目打包后的静态资源:比如js、css以及图片这些

对于完全不会变的资源:使用Cache-Control: max-age=31536000来为资源设置一个具体的缓存时间,并添加缓存校验(ETag和Last-Modified)。

现代的前端项目打包出来的静态资源,都会根据文件的内容,计算一个哈希值,并作为资源文件名的一部分,这个哈希值基本上等于这个文件的数据指纹,这样做的好处在于,当文件内容有变化时,其文件名就不一样,那么就可以直接将这种资源的缓存设置为:max-age=31536000,因为即使文件内容有更新了,文件名也变了,那么对应的资源url也变了,自然浏览器都会去下载新的资源了,而且这个缓存的时间设置的也比较长,不会频繁触发缓存校验。

这样来看,就可以直接将这些静态资源视为不会变的资源去设置缓存控制了。因为毕竟,缓存校验还是需要发送请求并让服务器进行校验,还是需要时间的。

可能会变的资源

如果资源可能会变,那么可以考虑使用缓存校验的形式,即不设置缓存的过期时间,而是使用Cache-Control: no-cache来让客户端每次都去校验这个资源的缓存是否过期了,如果没有过期则使用缓存,如果过期了,则下载新的资源。

通常来说,前端项目的index.html首页文件,都是需要使用缓存校验的形式去设置缓存的,因为我们每次项目更新,打包后的资源名称可能是不一样的,而html文件是引用这些资源的入口文件,所以,必须要保证html每次访问都是最新的,不能容忍客户端使用了一个过期的html文件。更狠一点可以设置资源为不缓存:Cache-Control: no-store,反正基本上单页面、没有服务端渲染的html文件的大小也不大。

参考