node强缓存和协商缓存实战示例

目录
  • 前言
  • 什么是浏览器缓存
    • 优点
  • 强缓存
    • Expires
    • Cache-Control
  • 协商缓存
    • ETag、If-None-Match
  • node实践
    • koa启动服务
      • 创建项目
      • koa代码
    • 原生koa实现简易静态资源服务
    • 强缓存验证
    • 设置Expire
    • 协商缓存验证
    • 小结
  • 总结

前言

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

什么是浏览器缓存

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

优点

减少了冗余的数据传输,节省带宽,减少服务器压力

加快了客户端加载速度,提升用户体验。

强缓存

强缓存不会向服务器发送请求,而是直接从缓存中读取资源,强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control,这两个头部分别是HTTP1.0和HTTP1.1的实现。

Expires

Expires是HTTP1.0提出的一个表示资源过期时间的header,它描述的是一个绝对时间,由服务器返回。

Expires 受限于本地时间,如果修改了本地时间,就会造成缓存失效。

Cache-Control

Cache-Control 出现于 HTTP/1.1,常见字段是max-age,单位是秒,很多web服务器都有默认配置,优先级高于Expires,表示的是相对时间。

例如Cache-Control:max-age=3600 代表资源的有效期是 3600 秒。取的是响应头中的 Date,请求发送的时间,表示当前资源在 Date ~ Date +3600s 这段时间里都是有效的。Cache-Control 还拥有多个值:

  • no-cache 不直接使用缓存,也就是跳过强缓存。
  • no-store 禁止浏览器缓存数据,每次请求资源都会向服务器要完整的资源。
  • public 可以被所有用户缓存,包括终端用户和 CDN 等中间件代理服务器。
  • private 只允许终端用户的浏览器缓存,不允许其他中间代理服务器缓存。

要注意的就是no-cache和no-store的区别,no-cache是跳过强缓存,还是会走协商缓存的步骤,而no-store是真正的完全不走缓存,所有资源都不会缓存在本地

协商缓存

当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串。

协商缓存用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】这两对Header来管理的。

注意!!协商缓存需要配合强缓存使用,使用协商缓存需要先设置Cache-Control:no-cache或者pragma:no-cache来告诉浏览器不走强缓存

Last-Modified、If-Modified-Since

这两个Header是HTTP1.0版本提出来的,两个字段配合使用。

Last-Modified 表示本地文件最后修改日期,浏览器会在请求头带上If-Modified-Since(上次返回的Last-Modified的值),服务器会将这个值与资源修改的时间匹配,如果时间不一致,服务器会返回新的资源,并且将 Last-Modified 值更新,作为响应头返回给浏览器。如果时间一致,表示资源没有更新,服务器返回 304 状态码,浏览器拿到响应状态码后从本地缓存中读取资源。

但Last-Modified有几个问题。

  • 文件虽然被修改了,但最终的内容没有变化,这样文件修改时间还是会被更新
  • 有的文件修改频率在秒以内,这时候以秒粒度来记录就不够了
  • 有的服务器无法精确获取文件的最后修改时间。

所以出现了ETAG。

ETag、If-None-Match

在HTTP1.1版本中,服务器通过 Etag 来设置响应头缓存标识。Etag 的值由服务端生成。在第一次请求时,服务器会将资源和 Etag 一并返回给浏览器,浏览器将两者缓存到本地缓存数据库。在第二次请求时,浏览器会将 Etag 信息放到 If-None-Match 请求头去访问服务器,服务器收到请求后,会将服务器中的文件标识与浏览器发来的标识进行对比,如果不相同,服务器返回更新的资源和新的 Etag ,如果相同,服务器返回 304 状态码,浏览器读取缓存。

流程总结

总结这几个字段:

  • Cache-Control —— 请求服务器之前
  • Expires —— 请求服务器之前
  • If-None-Match (Etag) —— 请求服务器
  • If-Modified-Since (Last-Modified) —— 请求服务器

node实践

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

koa启动服务

秉着学习和更容易理解的宗旨,不使用koa-static和koa-router中间件,用koa简易实现web服务器来验证之前的结论。

创建项目

# 创建并进入一个目录并新建index.js文件
mkdir koa-cache
cd koa-cache
touch index.js

# 初始化项目
git init
yarn init

# 将 koa 安装为本地依赖
yarn add koa

koa代码

/*app.js*/
const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
    ctx.body = 'hello koa'
})

app.listen(3000, () => {
  console.log('starting at port 3000')
})

启动服务

node index.js

这样一个koa服务就起来了,访问localhost:3000可以就看到hello koa。

为了方便调试,修改代码不用重新启动,推荐使用nodemon或者pm2启动服务。

原生koa实现简易静态资源服务

实现一个静态资源服务器关键点就是根据前端请求的地址来判断请求的资源类型,设置返回的Content-Type,让浏览器知道返回的内容类型,浏览器才能决定以什么形式,什么编码来读取返回的内容。

定义资源类型列表

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]
}

fs读取文件

const parseStatic = (dir) => {
  return new Promise((resolve) => {
    resolve(fs.readFileSync(dir), 'binary')
  })
}

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 {
    ctx.set('Content-Type', parseMime(url))
    ctx.body = await parseStatic(path.relative('/', url))
  }
})

这样基本也就完成了一个简单的静态资源服务器。然后在根目录下新建一个html文件和static目录,并在static下放一些文件。这时候的目录应该是这样的:

|-- koa-cache
    |-- index.html
    |-- index.js
    |-- static
        |-- css
            |-- color.css
            |-- ...
        |-- image
            |-- soldier.png
            |-- ...
        ...
   ...

这时候就可以通过localhost:3000/static访问具体的资源文件了。

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>test cache</title>
   <link rel="stylesheet" href="/static/css/index.css" rel="external nofollow"  />
 </head>
 <body>
   <div id="app">测试css文件</div>
   <img src="/static/image/soldier.png" alt="" />
 </body>
</html>

css/color.css

#app {
  color: blue;
}

这时候打开localhost:3000,就能看到如下效果:

到这里基本的环境就都搭好了。接下来进入验证阶段。

强缓存验证

在没有任何配置之前,可以看下network:

这时候无论是首次还是第几次,都会向服务器请求资源。

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

设置Expire

修改index.js中的app.use代码段。

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))
    // 设置过期时间在30000毫秒,也就是30秒后
    ctx.set('Expires', new Date(Date.now() + 30000))
    ctx.body = await parseStatic(filePath)
  }
})

用ctx.set(‘Expires’, new Date(Date.now() + 30000)),设置过期时间为当期时间的30000毫秒,也就是30秒后(后面的设置头部信息都是这里修改)。

再访问下localhost:3000,可以看到多了Expires这个Header。

后面在30秒之内访问都可以看到network的Size,css文件显示的是disk cache,而image资源显示的是from memory cache。这时候浏览器是直接读的浏览器缓存,并没有请求服务器,可以尝试把css和图片文件改名称或者删除验证下,页面显示正常,说明之前的结论是没错的。

Cache-Control

ctx.set(‘Cache-Control’, ‘max-age=300’)设置300秒有效期,验证方式同上。

协商缓存验证

Last-Modified,If-Modified-Since

HTTP1.0协商缓存关键点就是根据客户端请求带的ifModifiedSince字段的时间和请求的资源对应的修改时间来判断资源是否有更新。

首先设置Cache-Control: no-cache, 使客户端不走强缓存,再判断客户端请求是否有带ifModifiedSince字段,没有就设置Last-Modified字段,并返回资源文件。如果有就用fs.stat读取资源文件的修改时间,并进行对比,如果时间一样,则返回状态码304。

 ctx.set('Cache-Control', 'no-cache')
 const ifModifiedSince = ctx.request.header['if-modified-since']
 const fileStat = await getFileStat(filePath)
 if (ifModifiedSince === fileStat.mtime.toGMTString()) {
    ctx.status = 304
 } else {
    ctx.set('Last-Modified', fileStat.mtime.toGMTString())
    ctx.body = await parseStatic(filePath)
 }

etag、If-None-Match

etag的关键点在于计算资源文件的唯一性,这里使用nodejs内置的crypto模块来计算文件的hash值,并用十六进制的字符串表示。cypto的用法可以看nodejs的官网
。crpto不仅支持字符串的加密,还支持传入buffer加密,作为nodejs的内置模块,在这里用来计算文件的唯一标识再合适不过。

    ctx.set('Cache-Control', 'no-cache')
    const fileBuffer = await parseStatic(filePath)
    const ifNoneMatch = ctx.request.headers['if-none-match']
    const hash = crypto.createHash('md5')
    hash.update(fileBuffer)
    const etag = `"${hash.digest('hex')}"`
    if (ifNoneMatch === etag) {
      ctx.status = 304
    } else {
      ctx.set('etag', etag)
      ctx.body = fileBuffer
    }

效果如下图,第二次请求浏览器会带上If-None-Match,服务器计算文件的hash值再次比较,相同则返回304,不同再返回新的文件。而如果修改了文件,文件的hash值也就变了,这时候两个hash不匹配,服务器则返回新的文件并带上新文件的hash值作为etag。

小结

通过以上代码实践了每个缓存字段的效果,代码仅作为演示,生产的静态资源服务器会更加复杂,例如etag不会每次都重新获取文件来计算文件的hash值,这样太费性能,一般都会有响应的缓存机制,比如对资源的 last-modified 和 etag 值建立索引缓存。

总结

通常web服务器都有默认的缓存配置,具体的实现可能也不大相同,像nginx、tomcat、express等web服务器都有相应的源码,有兴趣的可以去阅读学习。

合理的使用强缓存和协商缓存具体需要看项目的使用场景和需求。像目前常见的单页面应用,因为通常打包都是新生成html与相应的静态资源依赖,所以可以对html文件配置协商缓存,而打包生成的依赖,例如js、css这些文件可以使用强缓存。或者只对第三方库使用强缓存,因为第三方库通常版本更新较慢,可以锁定版本。

node示例完整代码 https://github.com/chen-junyi/code/blob/main/node/cache/koa2.js

以上就是node强缓存和协商缓存实战示例的详细内容,更多关于node强缓存协商缓存的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解浏览器缓存和webpack缓存配置

    浏览器缓存 浏览器缓存分为两种类型: 强缓存:也称为本地缓存,不向服务器发送请求,直接使用客户端本地缓存数据 协商缓存:也称304缓存,向服务器发送请求,由服务器判断请求文件是否发生改变.如果未发生改变,则返回304状态码,通知客户端直接使用本地缓存:如果发生改变,则直接返回请求文件. 浏览器缓存机制的过程如下: 强缓存(本地缓存) 强缓存是最彻底的缓存,无需向服务器发送请求,通常用于css.js.图片等静态资源.浏览器发送请求后会先判断本地是否有缓存.如果无缓存,则直接向服务器发送请求:如果有

  • 实践示例理解js强缓存协商缓存

    目录 背景 前置准备 准备 启动页面 HTTP缓存种类 强缓存 expires cache-control 协商缓存 Last-Modified,If-Modified-Since Etag,If-None-Match 总结 背景 无论是开发中或者是面试中,HTTP缓存都是非常重要的,这体现在了两个方面: 开发中:合理利用HTTP缓存可以提高前端页面的性能 面试中:HTTP缓存是面试中的高频问点 所以本篇文章,我不讲废话,我就通过Nodejs的简单实践,给大家讲最通俗易懂的HTTP缓存,大家通过

  • js前端面试常见浏览器缓存强缓存及协商缓存实例

    目录 前言 搭建环境 强缓存 协商缓存 Etag和If-None-Match Last-Modify和if-modified-since 前言 最近在背面试题时,时常会看见浏览器缓存,虽然没有用过但是从它的描写中大致是知道它的作用和重要性.但是还是没有代码实操过,也是一知半解的,这口气咽不下啊,开始找资料,但是大部分都是理论半行代码没有,终于东拼西凑顿悟了.开始搭环境,干活. 浏览器缓存 浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本

  • vue增加强缓存和版本号的实现方法

    强缓存: 到底什么是强缓存?强在哪?其实强是强制的意思.当浏览器去请求某个文件的时候,服务端就在respone header里面对改文件做了缓存配置.缓存的时间.缓存类型都由服务端控制. 强缓存实现: cache-control: max-age=315360000, public ,immutable 客户端和代理服务器都可以缓存该资源,在315360000秒(10年)的有效期内,如果有请求该资源的需求的话就直接读取缓存,statu code:200 ,即使用户做了刷新操作,也不向服务器发起h

  • 详解浏览器的缓存机制

    目录 前言 1 浏览器缓存 1.1 浏览器缓存 1.2 浏览器缓存的意义 2 缓存类型 2.1 第一次请求数据 2.2 强制缓存 2.3 协商缓存 2.4 强制缓存和协商缓存的关系 3 缓存相关header 3.1 强制缓存 3.2 协商缓存 3.3 缓存请求 4 实例分析 4.1 官网首页: 4.2 社区 4.3 云市场 4.4 个人中心 4.5 论坛 4.6 App 总结 前言 浏览器缓存是前端性能优化的重要一环,对于前端效率提升的重要性,不言而喻. 之前对于浏览器缓存也是一知半解,这次借着

  • node强缓存和协商缓存实战示例

    目录 前言 什么是浏览器缓存 优点 强缓存 Expires Cache-Control 协商缓存 ETag.If-None-Match node实践 koa启动服务 创建项目 koa代码 原生koa实现简易静态资源服务 强缓存验证 设置Expire 协商缓存验证 小结 总结 前言 浏览器缓存是性能优化非常重要的一个方案,合理地使用缓存可以提高用户体验,还能节省服务器的开销.掌握好缓存的原理和并合理地使用无论对前端还是运维都是相当重要的. 什么是浏览器缓存 浏览器缓存(http 缓存) 是指浏览器

  • 10分钟彻底搞懂Http的强制缓存和协商缓存(小结)

    浏览器缓存 浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档. 所以根据上面的特点,浏览器缓存有下面的优点: 减少冗余的数据传输 减少服务器负担 加快客户端加载网页的速度 浏览器缓存是Web性能优化的重要方式.那么浏览器缓存的过程究竟是怎么样的呢? 在浏览器第一次发起请求时,本地无缓存,向web服务器发送请求,服务器起端响应请求,浏览器端缓存.过程如下: 在第一次请求时,服务器会将页面最后修改时间通过Last-Modifi

  • 基于nginx设置浏览器协商缓存过程详解

    这篇文章主要介绍了基于nginx设置浏览器协商缓存过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 强缓存与协商缓存的区别 强缓存:浏览器不与服务端协商直接取浏览器缓存 协商缓存:浏览器会先向服务器确认资源的有效性后才决定是从缓存中取资源还是重新获取资源 协商缓存运作原理 现在有一个这样的业务情景:后端的静态资源会不定时地发生更新,而因为浏览器默认使用强缓存,会默认从浏览器缓存中取到过时的资源. 现在我们希望浏览器每次获取资源的时候都向后

  • golang cache带索引超时缓存库实战示例

    目录 正文 定义泛型函数 Filter 函数 Map 函数 First 函数 带超时的cache cache 结构 集合操作 set 结构 带索引的cache index 结构 正文 cache 是一个带索引带超时的缓存库 目的在于优化代码结构,提供了若干实践. https://github.com/weapons97/cache example 定义泛型函数 1.18 已经发布一段实践了.通过泛型函数.我们可以减少循环的使用,优化代码结构.下面分享几个泛型函数和代码上的实践. Filter 函

  • 详解高性能缓存Caffeine原理及实战

    目录 一.简介 二.Caffeine 原理 2.1.淘汰算法 2.1.1.常见算法 2.1.2.W-TinyLFU 算法 2.2.高性能读写 2.2.1.读缓冲 2.2.2.写缓冲 三.Caffeine 实战 3.1.配置参数 3.2.项目实战 四.总结 一.简介 下面是Caffeine 官方测试报告. 由上面三幅图可见:不管在并发读.并发写还是并发读写的场景下,Caffeine 的性能都大幅领先于其他本地开源缓存组件. 本文先介绍 Caffeine 实现原理,再讲解如何在项目中使用 Caffe

  • Node.js基础入门之缓存区与文件操作详解

    目录 缓存区 1. 什么是缓存区? 2. 创建指定长度的缓存区 3. 通过数组创建缓存区 4. 通过字符串创建缓存区 5. 读写缓存区 6. 复制缓存区 文件操作 1. 异步直接读取 2. 同步直接读取 3. 流式读取 4. 写入文件 5. 流式写入文件 6. 读取文件信息 7. 删除文件 8. 管道 9. 链式流 经过前面三天的学习,Node.js的基础知识已逐渐掌握,今天继续学习缓存区和文件操作,并稍加整理加以分享,如有不足之处,还请指正. 缓存区 1. 什么是缓存区? JavaScript

  • Android JNI 调用时缓存字段和方法ID示例

    在 JNI 去调用 Java 的方法和访问字段时,最先要做的操作就是获得对应的类以及对应的方法 id. 事实上,通过 FindClass .GetFieldID.GetMethodID 去找到对应的信息是很耗时的,如果方法被频繁调用,那么肯定不能每次都去查找对应的信息,有必要将它们缓存起来,在下一次调用时,直接使用缓存内容就好了. 缓存有两种方式,分别是使用时缓存和初始化时缓存. 使用时缓存 使用时缓存,就是在调用时查找一次,然后将它缓存成 static 变量,这样下次调用时就已经被初始化过了.

随机推荐