http缓存

223 阅读11分钟

一、缓存分类

前端缓存可分为两大类:http 缓存 和 浏览器缓存。我们今天重点讲的是 http 缓存,所以关于浏览器缓存大家自行去查阅。下面这张图是前端缓存的一个大致知识点:

image.png

强缓存:浏览器直接从本地缓存中获取数据,不与服务器进行交互。

协商缓存:浏览器发送请求到服务器,服务器判定是否可使用本地缓存。

联系与区别:两种缓存方式最终使用的都是本地缓存;前者无需与服务器交互,后者需要。

二、HTTP缓存

1、什么是 HTTP 缓存 ?

http 缓存指的是: 当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求资源”的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源。

常见的 http 缓存只能缓存 get 请求响应的资源,对于其他类型的响应则无能为力,所以后续说的请求缓存都是指 GET 请求。

http 缓存都是从第二次请求开始的。第一次请求资源时,服务器返回资源,并在respone header 头中回传资源的缓存参数;

第二次请求时,浏览器判断这些请求参数,命中强缓存就直接 200,否则就把请求参数加到 request header 头中传给服务器,看是否命中协商缓存,命中则返回 304,否则服务器会返回新的资源。

2、http 缓存的分类

根据是否需要重新向服务器发起请求来分类,可分为( 强制缓存,协商缓存 );

根据是否可以被单个或者多个用户使用来分类,可分为 ( 私有缓存,共享缓存 )

强制缓存如果生效,不需要再和服务器发生交互,而协商缓存不管是否生效,都需要与服务端发生交互。下面是强制缓存和协商缓存的一些对比:

image

3、强制缓存

强制缓存在缓存数据未失效的情况下(即 Cache-Control 的 max-age 没有过期或者 Expires 的缓存时间没有过期),那么就会直接使用浏览器的缓存数据,不会再向服务器发送任何请求。

强制缓存生效时,http 状态码为 200。这种方式页面的加载速度是最快的,性能也是很好的,但是在这期间,如果服务器端的资源修改了,页面上是拿不到的,因为它不会再向服务器发请求了。这种情况就是我们在开发种经常遇到的,比如你修改了页面上的某个样式,在页面上刷新了但没有生效,因为走的是强缓存,所以 Ctrl + F5一顿操作之后就好了。 跟强制缓存相关的 header 头属性有 (Pragma/Cache-Control/Expires)

image.png

这个 Pragma 和 Cache-Control 共存时的优先级问题还有点异议,我在不同的文章里发现:有的说 Pragma 的优先级更高,有的说 Cache-Control 高。为了搞清楚这个问题,我决定动手操作一波,首先我用 nodejs 搭建后台服务器,目的是设置缓存参数,具体代码如下:

const http = require("http");
const fs = require("fs");

// 创建 http 服务
http.createServer((req, res) => {
    let html = fs.readFileSync("./www/index.html", "utf-8");

    // 访问首页
    if (req.url === "/") {
      res.writeHead(200, {
        "Content-Type": "text/html;charset=utf-8",
      });
      res.end(html);
    }
    // 访问脚本
    if (req.url === "/js/script.js") {
      res.writeHead(200, {
        "Content-Type": "text/javascript;chartset=utf-8",
        "cache-control": "max-age=30",
        'pragma': "no-cache",
      });
      res.end("max-age=30");
    }
    // 访问样式
    if (req.url === "/css/test.css") {
      res.writeHead(200, {
        "Content-Type": "text/css",
        "cache-control": "max-age=30",
        'Last-Modified': "Sun, 19 Aug 2023 15:52:12 GMT",
      });
      res.end('max-age=30')
    }
  }).listen(8888);

然后再浏览器上访问:http://127.0.0.1:8888 第一次访问时都是从后台返回的数据:

image

第二次访问时:

image

image

最终得出结论: Pragma 和 Cache-control 共存时,Pragma 的优先级是比 Cache-Control 高的

注意: 在 chrome 浏览器中返回的 200 状态会有两种情况:

1、from memory cache (从内存中获取/一般缓存更新频率较高的js、图片、字体等资源)

2、from disk cache (从磁盘中获取/一般缓存更新频率较低的js、css等资源)

这两种情况是 chrome 自身的一种缓存策略,这也是为什么 chrome 浏览器响应的快的原因。其他浏览返回的是已缓存状态,没有标识是从哪获取的缓存。

chrome浏览器:

image

Firefox浏览器:

image

4、协商缓存

当第一次请求时服务器返回的响应头中没有 Cache-Control 和 Expires 或者 Cache-Control 和 Expires 过期还或者它的属性设置为 no-cache 时(即不走强缓存),那么浏览器第二次请求时就会与服务器进行协商,与服务器端对比判断资源是否进行了修改更新。如果服务器端的资源没有修改,那么就会返回 304 状态码,告诉浏览器可以使用缓存中的数据,这样就减少了服务器的数据传输压力。

如果数据有更新就会返回 200 状态码,服务器就会返回更新后的资源并且将缓存信息一起返回。跟协商缓存相关的 header 头属性有(ETag/If-Not-Match 、Last-Modified/If-Modified-Since)请求头和响应头需要成对出现

image

协商缓存的执行流程是这样的

当浏览器第一次向服务器发送请求时,会在响应头中返回协商缓存的头属性:ETag 和 Last-Modified ,其中 ETag 返回的是一个 hash 值,Last-Modified 返回的是 GMT 格式的最后修改时间。

然后浏览器在第二次发送请求的时候,会在请求头中带上与 ETag 对应的 If-Not-Match,其值就是响应头中返回的 ETag 的值,Last-Modified 对应的 If-Modified-Since。服务器在接收到这两个参数后会做比较,如果返回的是 304 状态码,则说明请求的资源没有修改,浏览器可以直接在缓存中取数据,否则,服务器会直接返回数据。

image

image

注意: ETag/If-Not-Match 是在HTTP/1.1出现的,主要是解决以下问题:

(1)、Last-Modified(上次修改时间) 标注的最后修改只能精确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间

(2)、如果某些文件被修改了,但是内容并没有任何变化,而 Last-Modified 却改变了,导致文件没法使用缓存

(3)、有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形

5、私有缓存(浏览器级缓存)

私有缓存只能用于单独的用户:Cache-Control: Private

6、共享缓存(代理级缓存)

共享缓存可以被多个用户使用: Cache-Control: Public

7、为什么要使用 HTTP 缓存

根据上面的学习可发现使用缓存的好处主要有以下几点:

  1. 减少了冗余的数据传输,节省了网费。
  2. 缓解了服务器的压力, 大大提高了网站的性能
  3. 加快了客户端加载网页的速度

8、如何使用 HTTP 缓存 ?

一般需要缓存的资源有 html 页面和其他静态资源:

1、html 页面缓存的设置主要是在标签中嵌入标签,这种方式只对页面有效,对页面上的资源无效

1.1、html页面禁用缓存的设置如下:

仅有 IE 浏览器才识别的标签,不一定会在请求字段加上 Pragma,但的确会让当前页面每次都发新请求

<meta http-equiv="pragma" content="no-cache">

其他主流浏览器识别的标签

<meta http-equiv="cache-control" content="no-cache">

仅有 IE 浏览器才识别的标签,该方式仅仅作为知会 IE 缓存时间的标记,你并不能在请求或响应报文中找到 Expires 字段

<meta http-equiv="expires" content="0">

1.2、html 设置缓存如下:

其他主流浏览器识别的标签

<meta http-equiv="Cache-Control" content="max-age=7200" />

仅有 IE 浏览器才识别的标签

<meta http-equiv="Expires" content="Mon, 20 Aug 2018 23:00:00 GMT" />

2、静态资源的缓存一般是在web服务器上配置的,常用的web服务器有:nginx、apache。具体的配置这里不做详细介绍,大家自行查阅。

3、不想使用缓存的几种方式:

  • Ctrl + F5 强制刷新,都会直接向服务器提取数据。
  • 按 F5 刷新或浏览器的刷新按钮,默认加上 Cache-Control:max-age=0,即会走协商缓存。
  • 在 IE 浏览器下不想使用缓存的做法:打开 IE,点击工具栏上的工具->Internet选项->常规->浏览历史记录 设置. 选择“从不”,然后保存。最后点击“删除”把 Internet 临时文件都删掉 (IE缓存的文件就是 Internet 临时文件)。
  • 还有就是上面1、2中禁用缓存的做法
  • 对于其他浏览器也都有清除缓存的办法

9、HTTP 缓存的几个注意点

1、强缓存情况下,只要缓存还没过期,就会直接从缓存中取数据,就算服务器端有数据变化,也不会从服务器端获取了,这样就无法获取到修改后的数据。

解决的办法有:在修改后的资源加上随机数,确保不会从缓存中取。

例如: www.kimshare.club/kim/common.… www.kimshare.club/kim/common.…

2、尽量减少304的请求,因为我们知道,协商缓存每次都会与后台服务器进行交互,所以性能上不是很好。从性能上来看尽量多使用强缓存。

3、在 Firefox 浏览器下,使用 Cache-Control: no-cache 是不生效的,其识别的是 no-store。这样能达到其他浏览器使用 Cache-Control: no-cache 的效果。所以为了兼容 Firefox 浏览器,经常会写成 Cache-Control: no-cache,no-store。

4、与缓存相关的几个 header 属性有:Vary、Date/Age。

Vary: vary本身是“变化”的意思,而在 http 报文中更趋于是“vary from”(与。。。不同)的含义,它表示服务端会以什么基准字段来区分、筛选缓存版本。 在服务端有着这么一个地址,如果是 IE 用户则返回针对 IE 开发的内容,否则返回另一个主流浏览器版本的内容。 格式:Vary: User-Agent 知会代理服务器需要以 User-Agent 这个请求首部字段来区别缓存版本,防止传递给客户端的缓存不正确。

Date/Age: 响应报文中的 Date 和 Age 字段:区分其收到的资源是否命中了代理服务器的缓存。 Date 理所当然是原服务器发送该资源响应报文的时间(GMT格式),如果你发现 Date 的时间与“当前时间”差别较大,或者连续F5刷新发现 Date 的值都没变化,则说明你当前请求是命中了代理服务器的缓存。 Age 也是响应报文中的首部字段,它表示该文件在代理服务器中存在的时间(秒),如文件被修改或替换,Age会重新由0开始累计。

三、 浏览器缓存

下面说说最常用到的浏览器缓存有:cookie、sessionStorage、localStorage 这三者的主要特征如下:

image

四、课程总结

1、对于强制缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行协商缓存策略。

2、对于协商缓存,将缓存信息中的 Etag 和 Last-Modified 通过请求发送给服务器,由服务器校验,返回 304 状态码时,浏览器直接使用缓存。

下图是浏览器首次和再次发送http请求的执行流程图:

image

image

五、设置强缓存及协商缓存示例代码:

const express = require('express')
const app = express()

// 强缓存,设置一个过期时间(浏览器,第一次收到相应头开始计时,浏览器把收到的资源-字符串,保存到内存或硬盘中);
// 在缓存期:服务端修改静态资源 无效。浏览器只读取内存 或者 硬盘中数据
app.use(function (req, res, next) {
  console.log('req:'+req.url)
  res.setHeader('cache-control', 'max-age=1500')
  next()
})


// 协商缓存
const _tag = '887766534412234444444444444'
app.use(function (req, res, next) {
  // 这是每次请求都会向浏览器设置
  res.setHeader('etag', _tag)
  // 第二次浏览器来请求,才会有这个if-no-match
  const reqIfNo = req.header('If-None-Match')
  console.log(reqIfNo)
  if (reqIfNo === _tag) {
    console.log('走304')
    res.status(304)
    res.send('2222')
  } else {
    next()
  }
})


// 上面的是为了理解,自己手写的,下面的是框架自带的。真正工作,是用下面这个配置
app.use('/', express.static('./www', {
  maxAge: 60*60*1000, // 开启强缓存
  // etag: true, // 开启协商缓存
  // lastModified: true, // 开启协商缓存
}))

app.listen(3333, () => console.log(3333))