缓存是一种保留资源正本并在下次申请时间接应用该正本的技术,通过复用以前获取的资源,能够显着进步网站性能,升高服务器解决压力,缩小了等待时间和网络流量。通过应用 HTTP缓存,变得更加响应性。

这篇文章会介绍三种缓存机制在nodejs中的实现,别离是:

  • 强制缓存 Cache-Control/Expires
  • 比照缓存

    • Last-Modified/If-Modified-Since
    • Etag/If-None-Match

两类缓存规定的不同

  • 强制缓存如果失效,不须要再和服务器产生交互,比照缓存不论是否失效,都须要与服务端产生交互。
  • 强制缓存优先级高于比照缓存强制缓存失效时,不再执行比照缓存规定。

新建http服务

为了不便测试,新建一个简略的http服务进行调试:

const http = require("http")const url = require("url")const mime = require("mime")const fs = require("fs")const server = http.createServer((req, res) => {  const { pathname } = url.parse(req.url, true)  const abspath = process.cwd() + pathname  fs.stat(abspath, handleRequest)  // 判断是文件还是文件夹  function handleRequest(err, statObj) {    if (err || statObj.isDirectory()) return sendError(err)    sendFile()  }  // 响应谬误申请  function sendError(error) {    res.statusCode = 404    res.end(`Not Found \r\n ${error.toString()}`)  }  // 响应文件申请  function sendFile() {    res.setHeader("Content-Type", mime.getType(abspath) + ";charset:utf-8")    fs.createReadStream(abspath).pipe(res)  }})server.listen(3000, () => console.log("serving http://127.0.0.1:3000"))

强制缓存

强制缓存指的是在缓存数据未生效的状况下,能够间接应用缓存数据,浏览器通过服务器响应的header获取缓存规定信息。对于强制缓存,响应头header应用Cache-Control/Expires来表明生效规定。

Expires

Expires是HTTP1.0的货色,当初默认浏览器均默认应用HTTP 1.1,所以它的作用根本疏忽,咱们在响应头header设置Expires,浏览器依据它的到期工夫来决定是否应用缓存数据:

res.setHeader("Expries",new Date(Date.now()+5*1000).toUTCString());

Cache-Control

Cache-Control是最重要的规定。常见的取值有private、public、no-cache、max-age,no-store,默认为private。

  • private 客户端能够缓存
  • public 能够被任何中间人(比方两头代理、CDN等)缓存
  • max-age=xxx 缓存的内容将在 xxx 秒后生效(单位是秒)
  • no-cache 须要应用比照缓存来验证缓存数据
  • no-store 所有内容都不会缓存,强制缓存比照缓存都不会触发

能够在handleRequest办法中增加给响应头设置Cache-Control,在浏览器刷新查看成果:

function handleRequest(err, statObj) {    ...    res.setHeader("Cache-Control","max-age=10");    sendFile()}

如果常常调试前端我的项目的开发人员,常常会把控制台Disable cache给勾上,这里记得肯定要关掉它:

不出意外的话,能够在Network的申请中看到信息:

Status Code: 200 OK (from disk cache)

比照缓存

比照缓存,服务器将文件的批改信息发送给客户端(浏览器),客户端在下次申请的时候顺便带上,而后服务端就能够拿到上一次的批改信息,跟本地的文件批改信息做比拟,通知客户端是否用缓存数据还是用最新数据了

Last-Modified/If-Modified-Since 比照工夫

通过statObjctime属性能够获取文件的批改工夫,将这个批改信息通过申请头Last-Modified属性发送给浏览器:

const serverTime = statObj.ctime.toUTCString()res.setHeader("Last-Modified", serverTime)

下次客户端申请的时候,也会在申请头通过if-modified-since带上:

const clientTime = req.headers["if-modified-since"]

批改handleRequest办法如下:

function handleRequest(err, statObj) {    if (err || statObj.isDirectory()) return sendError(err)    const clientTime = req.headers["if-modified-since"]    const serverTime = statObj.ctime.toUTCString()    // 如果本地的文件批改工夫和浏览器返回的批改工夫雷同,则应用缓存的数据,返回304    if (clientTime === serverTime) {        res.statusCode = 304        return res.end()    }    res.setHeader("Last-Modified", serverTime)    res.setHeader("Cache-Control", "no-cache") //  比照缓存验证缓存数据    sendFile()  }

不过这种形式有两个弊病:

  • 如果一个文件被误批改,而后批改被撤销,这样内容尽管没变动,但最新批改工夫会变
  • 文件被周期性的批改,文件内容没有变动,但最新批改工夫会变动

Etag/If-None-Match 比照内容

下面提到的两个弊病,能够通过Etag/If-None-Match形式解决,也就是内容比照,不过Etag生成有肯定的开销,如果文件频繁变动对服务器有额定压力。

当然咱们不可能将内容都存在header外面,这里能够通过crypto将文件内容加密成一串秘钥,写在headerEtag属性中:

const crypto = require("crypto");...const md5 = crypto.createHash("md5"); // md5加密const rs = fs.createReadStream(abspath); // 创立可读流const data = [];rs.on("data",(buffer)=>{    md5.update(buffer); // 读取文件内容过程中加密    data.push(buffer);});rs.on("end",()=>{    const serverMatch = md5.digest("base64"); // 加密后的文件    const clientMatch = req.headers["if-none-match"]; // 客户端下次申请会带上serverMatch    if(clientMatch === serverMatch){ // 比照文件内容是否雷同        res.statusCode = 304;        return res.end(null);    }    // 设置 ETag    res.setHeader("ETag", serverMatch)    res.end(Buffer.concat(data));})

整合

咱们能够在业务中依据本人的须要,设置对应的缓存形式,这里通过写个通用办法,将这三种模式整合起来:

  function cache(statObj) {    // 强制缓存    res.setHeader("Cache-Control", "max-age=60")    // 工夫比照    let lastModified = statObj.ctime.toUTCString()    let modifiedSince = req.headers["if-modified-since"]    res.setHeader("Last-Modified", lastModified)    if (modifiedSince !== lastModified) return false    // 内容比照    let etag = statObj.size + ""    let noneMatch = req.headers["if-none-match"]    res.setHeader("ETag", etag)    if (etag !== noneMatch) return false    return true  }  ...  if(cache(statObj)){    res.statusCode = 304    return res.end(null)  }

参考文章:
彻底弄懂HTTP缓存机制及原理
HTTP 缓存
协商缓存