概念

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。——MDN

缓存能缓解服务器压力,提高响应速度,提升用户体验。

以下讨论的缓存是针对对img/script/css资源而言的,且缓存策略都是依靠 http 报文的首部来实现。

实验

搭建实验环境

编写 html 文件

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="icon" href="data:;base64,=">
  <title>缓存</title>
</head>

<body>
  <img style="width:400px;" src="sun.jpg">
  <div>
    缓存实验
  </div>
</body>

</html>

服务端代码

const path = require('path')
const url = require('url')
const fs = require('fs')
const zlib = require('zlib')

const img = fs.readFileSync(path.resolve(__dirname, './static/sun.jpg'), 'binary')

http.createServer((req, res) => {
  const html = fs.createReadStream(path.resolve(__dirname, './static/index.html'))
  let {
    pathname
  } = url.parse(req.url)
  if (pathname === '/') {
    res.writeHead(200, {
      'Content-Type': 'text/html',
      'Content-Encoding': 'gzip'
    })
    html.pipe(zlib.createGzip()).pipe(res)
  } else if (pathname === '/sun.jpg') {
    res.writeHead(200, {
      'Content-Type': 'image/jpeg'
    })
    res.write(img, 'binary')
    res.end()
  } else {
    res.end(pathname)
  }
}).listen(3210)

访问 http://localhost:3210 , 看到相应的文档和图片,且无论刷新多少次,sun.jpg 的 Status 和 Size 都不变。控制台里勾选 Preserve log

clipboard.png

Cache-Control

通过在服务端设置相应首部 Cache-Control 可以控制缓存行为,例如

  • Cache-Control: no-cache, 告诉浏览器,下次请求该资源时,不直接使用缓存,而是向服务端发送请求,服务端会根据请求,判断本地的资源是否过期
  • Cache-Control: no-store, 告诉浏览器,不要缓存该资源
  • Cache-Control: max-age=age, 告诉浏览器,这个资源有效的时长 age,在该时间范围内,如果需要该资源,直接从本地取,不要烦我(向我发送请求)

修改服务端代码

设置图片缓存时长 30s

res.writeHead(200, {
      'Content-Type': 'image/jpeg',
      'Cache-Control': 'max-age=30'
    })

快速刷新页面2次,超过30s后,再刷新一次

三次刷新结果在控制台显示的情况如下

clipboard.png
其中第二次请求 sun.jpg 的请求情况如下,200 OK (from memory cache),第三次请求 sun.jpg 则又是从服务端获取图片,显示资源 1.6M。

clipboard.png

Last-Modified & If-Modified

现在的情形是这样的,30s到了,浏览器向服务端发出 sun.jpg 的请求,但服务器并没有更新该资源,所以服务器告诉浏览器,“我没有修改该资源,你还是用你本地的吧,我就不再发给你了”。这些通过 Last-Modified 和 If-Modified-Since 来实现。

浏览器第一次向服务端请求 sun.jpg,服务端不仅返回 sun.jpg,还通过响应首部的 Last-Modified(浏览器自动添加),告诉浏览器该资源最后修改的时间。

下次浏览器再次请求 sun.jpg,会通过请求首部的 If-Modified-Since, 把自己手上这个资源最后修改的时间告诉服务端,服务端通过这个时间,判断浏览器本地的资源是否是最新的,若是,则返回 304(not modified) 和响应头部即可,不用返回图片;若不是,则返回 200 和图片。

修改服务端代码

const http = require('http')
const path = require('path')
const url = require('url')
const fs = require('fs')
const zlib = require('zlib')

const img = fs.readFileSync(path.resolve(__dirname, './static/sun.jpg'), 'binary')
let date = new Date()
let lastModified = date.toUTCString()

http.createServer((req, res) => {
  const html = fs.createReadStream(path.resolve(__dirname, './static/index.html'))
  let {
    pathname
  } = url.parse(req.url)
  if (pathname === '/') {
    res.writeHead(200, {
      'Content-Type': 'text/html',
      'Content-Encoding': 'gzip'
    })
    html.pipe(zlib.createGzip()).pipe(res)
  } else if (pathname === '/sun.jpg') {
    if (req.headers['if-modified-since'] === lastModified) {
      res.writeHead(304, {
        'Content-Type': 'image/jpeg',
        'Cache-Control': 'max-age=30',
        'Last-Modified': lastModified
      })
      res.end()
    } else {
      res.writeHead(200, {
        'Content-Type': 'image/jpeg',
        'Cache-Control': 'max-age=30',
        'Last-Modified': lastModified
      })
      res.write(img, 'binary')
      res.end()
    }
  } else {
    res.end(pathname)
  }
}).listen(3210)

浏览器刷新一次,30s内,再刷新一次,30s后再刷新一次

三次刷新的结果在控制台里显示如下,最后一次还有 189B 是服务端响应头部的大小,178ms 里包含了服务端比较修改时间,返回响应头部的时间。

clipboard.png

且最后一次请求 sun.jpg 的请求情形如下

clipboard.png

ETag & If-None-Match

Etag 和 If-None-Match 起到的作用和上文中 Last-Modified 和 If-Modified 差不多,区别在于 Etag 是服务端上通过算法给资源计算出的唯一标示,当资源修改时,该标示会发生变化。服务端通过响应首部 Etag 将该标示告诉浏览器,浏览器再下一次请求该资源时,会通过请求首部 If-None-Match 带上该标示,服务端会比较请求中的标示和该资源最新的标示,如果一样,证明浏览器拥有的该资源是最新的,仅返回304和响应头部,否则返回200和整个资源。

强制刷新

常使用的强制刷新就是通过修改请求首部来实现的

clipboard.png

将 Cache-Control 和 Pragma 设为 no-cache,去除 If-Modified, If-None-Match

盗个图,HTTP缓存控制小结

clipboard.png

快速更新

很多js文件或css文件的文件名会带有文件指纹或版本号,通过改变文件名,让浏览器以为请求的新的未缓存的资源从而实现快速更新。

clipboard.png

参考


nbb3210
436 声望31 粉丝

优雅地使用JavaScript解决问题