浏览器缓存

455 阅读7分钟

浏览器缓存

浏览器的缓存机制也就是我们说的HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的,主要是依据http头中相关字段进行的。

第一次请求

浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中。

第二次请求

浏览器第二次向服务器发起该请求可以分为3种情况:

  • 如果浏览器缓存中不存在缓存资源和缓存标识,强制缓存失效,直接向服务器发起请求。
  • 存在该缓存结果和缓存标识(Expires和Cache-Control),且该结果尚未失效,强制缓存生效,直接返回该结果且状态码为200。
  • 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存。

demo 验证

Talk is cheap, show me the code.

不对响应头做任何修改

创建一个简单的 server

index.js

const http = reqire('http')
http.createServer((req, res) => {
    const {url, headers} = req // 从请求数据中解构出 url 和 headers
    if (url === '/') {
      res.end(`
        <html>
          html update Time ${new Date()}
          <script src = '/main.js'></script>
        </html>
      `)
    } else if (url === '/main.js') {
        const content = `document.writeln('<br /> JS Update Time: ${new Date()}')`
        res.statusCode = 200
        res.end('content')
    }
    
}).listen(3000, () => {
    console.log('Http is running at 3000 port')
})

通过 node index.js 启动服务, 并在浏览器中访问 localhost:3000, 打开 network, 取消 Disable cache 并刷新浏览器, 会发现 html 和 js 的更新时间是同步的, 右侧请求返回的状态码是 200

强缓存

控制强制缓存的字段分别是Expires和Cache-Control,其中Cache-Control优先级比Expires高

Expires

想要触发浏览器的缓存, 只需要在响应头中添加 Expires 字段就可以, Expires 的值是格林尼治时间,可以是未来时间, 也可以是过去时间。

const getGMTTime = (time = Date.now()) => {
  return new Date(time).toGMTString()
}

index.js

const http = reqire('http')
http.createServer((req, res) => {
    const {url, headers} = req // 从请求数据中解构出 url 和 headers
    if (url === '/') {
      res.end(`
        <html>
          html update Time ${getGMTTime()}
          <script src = '/main.js'></script>
        </html>
      `)
    } else if (url === '/main.js') {
        const content = `document.writeln('<br /> JS Update Time: ${getGMTTime()}')`
        // 返回数据时,加上响应头
        res.setHeader('Expires', getGMTTime(Date.now() + 10 * 1000)) // 设置10秒后过期
        res.statusCode = 200
        res.end('content')
    }
    
}).listen(3000, () => {
    console.log('Http is running at 3000 port')
})

  • 我们可以看到响应头中多了一个Expires字段,并且可以看到通过刷新浏览器后, JS 的更新一直保持在14:57:49 而 html 的更新时间是 14:57:56, 并且可以看到 js 的请求状态码是200并且数据来源是 from memory cache。
  • 10 秒之后再次刷新, 浏览器会再次发送请求

注意

  • Expires 字段是 http 1.0 的东西, 现在浏览器多使用 http 1.1, 可以使用 Cache-Control 字段
  • Expires 的值是一个绝对时间,如果浏览器时间和服务器时间不同步, 浏览器时间设置了一个很未来的时间, 那么较短的过期时间是没有用的
  • 缓存过期后, 不管服务器上的资源是否有改动, 服务器都会重新读取资源并返回给浏览器。

Cache-Control

针对上面的前两个问题, 我们用 Cache-Control 来解决

Cache-Control 有很多可选值, 比较常用的有 max-age, no-cache, no-store, must-revalidate

max-age: max-age 的值是相对时间, 单位是秒。如 max-age=10 相当于告诉浏览器资源10秒后过期。有一点需要注意,max-age 的时间计算起点是响应报文的创建时刻(即Date 字段, 也就是离开服务器的时间),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。 no-store: 不允许缓存, 用于某些变化非常频繁的数据 no-cache: 可以缓存,但是使用之前必须要去服务器验证是否过期, 是否有最新的版本。 must-revalidate: 如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。

const http = reqire('http')
http.createServer((req, res) => {
    const {url, headers} = req // 从请求数据中解构出 url 和 headers
    if (url === '/') {
      res.end(`
        <html>
          html update Time ${getGMTTime()}
          <script src = '/main.js'></script>
        </html>
      `)
    } else if (url === '/main.js') {
        const content = `document.writeln('<br /> JS Update Time: ${getGMTTime()}')`
        // 返回数据时,加上响应头
        res.setHeader('Cache-Control', 'max-age=10, no-cahce') // 设置10秒后过期
        res.statusCode = 200
        res.end('content')
    }
    
}).listen(3000, () => {
    console.log('Http is running at 3000 port')
})

下面这两种情况,自己测试一下。
> Ctrl + F5 :强制刷新其实时发了一个 'Cache-Control: no-cache'
> 点击浏览器的'前进''后退' 按钮,再看开发者工具,会发现 'from disk cache', 是因为这么做浏览器只用最基本的请求头, 没有 'Cache-Control' ,所以就会检查缓存,直接利用之前的资源,不再进行网络通信。

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程

与协商缓存相关的字段有Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match优先级比Last-Modified / If-Modified-Since高

Last-Modified

http 协议定义了一系列 'If' 开头的 '条件请求'

常用的有: if-modified-since 和 if-none-match, 需要第一次的响应报文预先提供 'Last-Modified' 和 'Etag', 然后第二次请求时就可以带上缓存里的原值, 验证资源是否是最新的。 如果资源没有变, 服务器就回应一个 '304 Not Modified', 表示缓存依然有效, 浏览器就可以更新一下有效期, 然后放心大胆地使用缓存了。

Last-Modified 就是文件的最后修改时间,。

const http = reqire('http')
http.createServer((req, res) => {
    const {url, headers} = req // 从请求数据中解构出 url 和 headers
    if (url === '/') {
      res.end(`
        <html>
          html update Time ${getGMTTime()}
          <script src = '/main.js'></script>
        </html>
      `)
    } else if (url === '/main.js') {
        const content = `document.writeln('<br /> JS Update Time: ${getGMTTime()}')`
        // 返回数据时,加上响应头
        res.setHeader('Cache-Control', 'max-age=5, no-cahce') // 设置10秒后过期
        res.setHeader('Last-Modified', new Date())
        if(new Date(headers['if-modified-since']).getTime() + 10 * 1000 > Date.now()) {
            res.statusCode = 304 // 如果命中缓存, 则返回 304
            res.end()
            return
        }
        res.statusCode = 200
        res.end('content')
    }
    
}).listen(3000, () => {
    console.log('Http is running at 3000 port')
})

使用Last-Modified 会有以下问题

  • 一个文件在一秒内修改了多次
  • 文件定期更新,但内容没有变化

ETag

更精准地识别资源变动的情况

ETag 有 "强" "弱" 之分。 强 ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个“W/”标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。

Etag的工作原理 Etag在服务器上生成后,客户端通过If-Match或者说If-None-Match这个条件判断请求来验证资源是否修改.我们常见的是使用If-None-Match.请求一个文件的流程可能如下: 新的请求 客户端发起HTTP GET请求一个文件(css ,image,js);服务器处理请求,返回文件内容和一堆Header(包括Etag,例如"2e681a-6-5d044840"),http头状态码为为200.

同一个用户第二次这个文件的请求 客户端在一次发起HTTP GET请求一个文件,注意这个时候客户端同时发送一个If-None-Match头,这个头中会包括上次这个文件的Etag(例如"2e681a- 6-5d044840"),这时服务器判断发送过来的Etag和自己计算出来的Etag,因此If-None-Match为False(none match 为 false, 就是 match 了, 就是两个 Etag相同, 也就是说文件没改变),不返回200,返 回304,客户端继续使用本地缓存;

注意.服务器又设置了Cache-Control:max-age和Expires时,会同时使用,也就是说在完全匹配If-Modified-Since和If-None-Match即检查完修改时间和Etag之后,服务器才能返回304。