用node彻底搞懂强缓存和协商缓存

36 阅读3分钟

大家好,我是鹿萌萌, 如果觉得还不错,那就给个一键3连吧💗💗💗

前言

浏览器缓存是性能优化非常重要的一个方案,合理地使用缓存可以提高用户体验,还能节省服务器的开销。掌握好缓存的原理和并合理地使用无论对前端还是运维都是相当重要的。

最近萌萌再研究前端性能优化,无论是开发中或者是面试中,HTTP缓存都是非常重要的,这体现了两个方面:

  • 开发中:合理利用HTTP缓存可以提高前端页面的性能
  • 面试中HTTP缓存是面试中的高频问点

所以本篇文章,萌萌就通过Nodejs的简单实践,给大家讲讲HTTP缓存,大家通过这篇文章一定能了解并掌握它!

什么是浏览器缓存

浏览器缓存(http 缓存) 是指浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档。

node实践

本文用koa来做例子,因为koa是更轻量级的、更纯净的,本身并没有捆绑任何中间件,相比express自带了很多router、static等多种中间件函数,koa更适合本文来做示例。

准备

  • 创建文件夹cache-study,并准备环境
npm init
  • 安装Koa、nodemon
  • 修改代码不用重新启动,推荐使用nodemon或者pm2启动服务
npm i koa -D
npm i nodemon -g
  • 创建index.js、index.html、static文件夹
  • 目录如下:

image.png

  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./static/css/index.css">
</head>
<body>
  <div class="box">

  </div>
</body>
</html>
  • static/css/index.css
.box {
    width: 450px;
    height: 450px;
    background-image: url('../img/lumengmeng.jpg');
    background-size: 100% 100%;
}
  • index.js
const Koa = require('koa')
const fs = require('fs')
const path = require('path')
const mimes = {
  css: 'text/css',
  less: 'text/css',
  gif: 'image/gif',
  html: 'text/html',
  ico: 'image/x-icon',
  jpeg: 'image/jpeg',
  jpg: 'image/jpeg',
  js: 'text/javascript',
  json: 'application/json',
  pdf: 'application/pdf',
  png: 'image/png',
  svg: 'image/svg+xml',
  swf: 'application/x-shockwave-flash',
  tiff: 'image/tiff',
  txt: 'text/plain',
  wav: 'audio/x-wav',
  wma: 'audio/x-ms-wma',
  wmv: 'video/x-ms-wmv',
  xml: 'text/xml',
}

// 获取文件的类型
function parseMime(url) {
  // path.extname获取路径中文件的后缀名
  let extName = path.extname(url)
  extName = extName ? extName.slice(1) : 'unknown'
  return mimes[extName]
}

// 将文件转成传输所需格式
const parseStatic = (dir) => {
  return new Promise((resolve) => {
    resolve(fs.readFileSync(dir), 'binary')
  })
}

const app = new Koa()

app.use(async (ctx) => {
  const url = ctx.request.url
  if (url === '/') {
    // 访问根路径返回index.html
    ctx.set('Content-Type', 'text/html')
    ctx.body = await parseStatic('./index.html')
  } else {
    const filePath = path.resolve(__dirname, `.${url}`)
    // 设置类型
    ctx.set('Content-Type', parseMime(url))
    // 设置传输
    ctx.body = await parseStatic(filePath)
  }
})

app.listen(9898, () => {
  console.log('start at port 9898')
})

启动页面

现在你可以在终端中输入nodemon index,看到下方的显示,则代表成功启动了服务

image.png

此时你可以在浏览器链接里输入http://localhost:9898/,打开看到如下页面,则代表页面访问成功啦

image.png

HTTP缓存种类

HTTP缓存常见的有两类:

  • 强缓存:可以由这两个字段其中一个决定

    • expires
    • cache-control(优先级更高)
  • 协商缓存:可以由这两对字段中的一对决定

    • Last-Modified,If-Modified-Since
    • Etag,If-None-Match(优先级更高)

强缓存

接下来我们就先研究强缓存 在没有任何配置之前,可以看下network:

image.png 我们只需设置响应头里expires的时间为当前时间 + 30s就行了

app.use(async (ctx) => {
  const url = ctx.request.url
  if (url === '/') {
    // 访问根路径返回index.html
    ctx.set('Content-Type', 'text/html')
    ctx.body = await parseStatic('./index.html')
  } else {
    const filePath = path.resolve(__dirname, `.${url}`)
    // 设置类型
    ctx.set('Content-Type', parseMime(url))
    // 设置 Expires 响应头
    const time = new Date(Date.now() + 30000).toUTCString()
    ctx.set('Expires', time)
    // 设置传输
    ctx.body = await parseStatic(filePath)
  }
})

设置成功后我们再看下效果:

注意!!!在开始实验之前要把network面板的Disable cache勾选去掉,这个选项表示禁用浏览器缓存,浏览器请求会带上Cache-Control: no-cache和Pragma: no-cache头部信息,这时候所有的请求都不会走缓存

image.png

image.png

cache-control

其实cache-controlexpires效果差不多,只不过这两个字段设置的值不一样而已,前者设置的是秒数,后者设置的是毫秒数

app.use(async (ctx) => {
  const url = ctx.request.url
  if (url === '/') {
    // 访问根路径返回index.html
    ctx.set('Content-Type', 'text/html')
    ctx.body = await parseStatic('./index.html')
  } else {
    const filePath = path.resolve(__dirname, `.${url}`)
    // 设置类型
    ctx.set('Content-Type', parseMime(url))
    // 设置 Cache-Control 响应头
    ctx.set('Cache-Control', 'max-age=30')
    // 设置传输
    ctx.body = await parseStatic(filePath)
  }
})

前端页面响应头多了cache-control这个字段,且30s内都走本地缓存,不会去请求服务端

image.png

协商缓存

强缓存不同的是,强缓存是在时效时间内,不走服务端,只走本地缓存;而协商缓存是要走服务端的,如果请求某个资源,去请求服务端时,发现命中缓存则返回304,否则则返回所请求的资源,那怎么才算命中缓存呢?接下来讲讲

Last-Modified,If-Modified-Since

简单来说就是:

  • 第一次请求资源时,服务端会把所请求的资源的最后一次修改时间当成响应头中Last-Modified的值发到浏览器并在浏览器存起来
  • 第二次请求资源时,浏览器会把刚刚存储的时间当成请求头中If-Modified-Since的值,传到服务端,服务端拿到这个时间跟所请求的资源的最后修改时间进行比对
  • 比对结果如果两个时间相同,则说明此资源没修改过,那就是命中缓存,那就返回304,如果不相同,则说明此资源修改过了,则没命中缓存,则返回修改过后的新资源
// 获取文件信息
const getFileStat = (path) => {
  return new Promise((resolve) => {
    fs.stat(path, (_, stat) => {
      resolve(stat)
    })
  })
}

app.use(async (ctx) => {
  const url = ctx.request.url
  if (url === '/') {
    // 访问根路径返回index.html
    ctx.set('Content-Type', 'text/html')
    ctx.body = await parseStatic('./index.html')
  } else {
    const filePath = path.resolve(__dirname, `.${url}`)
    const ifModifiedSince = ctx.request.header['if-modified-since']
    const fileStat = await getFileStat(filePath)
    console.log(new Date(fileStat.mtime).getTime())
    ctx.set('Cache-Control', 'no-cache')
    ctx.set('Content-Type', parseMime(url))
    // 比对时间,mtime为文件最后修改时间
    if (ifModifiedSince === fileStat.mtime.toGMTString()) {
      ctx.status = 304
    } else {
      ctx.set('Last-Modified', fileStat.mtime.toGMTString())
      ctx.body = await parseStatic(filePath)
    }
  }
})

image.png

image.png

Etag,If-None-Match

其实Etag,If-None-MatchLast-Modified,If-Modified-Since大体一样,区别在于:

  • 后者是对比资源最后一次修改时间,来确定资源是否修改了
  • 前者是对比资源内容,来确定资源是否修改

那我们要怎么比对资源内容呢?我们只需要读取资源内容,转成hash值,前后进行比对就行了!

const crypto = require('crypto')

app.use(async (ctx) => {
  const url = ctx.request.url
  if (url === '/') {
    // 访问根路径返回index.html
    ctx.set('Content-Type', 'text/html')
    ctx.body = await parseStatic('./index.html')
  } else {
    const filePath = path.resolve(__dirname, `.${url}`)
    const fileBuffer = await parseStatic(filePath)
    const ifNoneMatch = ctx.request.header['if-none-match']
    // 生产内容hash值
    const hash = crypto.createHash('md5')
    hash.update(fileBuffer)
    const etag = `"${hash.digest('hex')}"`
    ctx.set('Cache-Control', 'no-cache')
    ctx.set('Content-Type', parseMime(url))
    // 对比hash值
    if (ifNoneMatch === etag) {
      ctx.status = 304
    } else {
      ctx.set('etag', etag)
      ctx.body = fileBuffer
    }
  }
})

image.png