一个小案例搞定HTTP缓存机制原理(强制缓存+协商缓存)- 图文描述

提示:浏览器缓存是性能优化的一个重要手段,对于理解缓存机制而言也是很重要的!


前言

背景:在任何一个前端项目中,访问服务器获取数据都是很常见的事情,但是如果相同的数据被重复请求了不止一次,那么多余的请求次数必然会增加浪费网络带宽,影响用户是使用体验。因此使用缓存技术对已经获取到的资源进行重用,是一种提升网站性能与用户体验的有效策略。


一、什么是缓存?

浏览器缓存(Brower Caching)是浏览器对之前请求过的文件进行缓存,以便下一次访问时重复使用,节省带宽,提高访问速度,降低服务器压力!


二、简述缓存的原理?

原理就是在首次请求保存一份请求资源的响应副本,当用户在此发起相同的请求后,如果判断缓存存在就拦截发送的请求,将之前存储的副本返回给用户,从而避免重新向服务器发送资源请求!


三、HTTP缓存设置的机制?

1:http缓存机制主要在http响应头中设定,响应头中相关字段为ExpiresCache-ControlLast-ModifiedEtag
2:配置服务器响应头来告诉浏览器是否应该缓存资源、是否强制校验缓存、缓存多长时间;浏览器非首次请求根据响应头是否应该取缓存、缓存过期发送请求头验证缓存是否可用还是重新获取资源的过程。


四、缓存分类?

1:根据是否需要重新向服务器发送请求分类:

HTTP缓存是前端开发中最常接触的缓存机制之一,具体来说可以分为强制缓存协商缓存
二者的区别:在于判断缓存命中时,浏览器是否需要向服务器端进行询问,进而判断是否需要根据响应内容重新进行请求。

2:根据是否可以被单个或者多个用户使用来分类:

根据是否可以被单个或者多个用户使用可以分为私有缓存共享缓存。共享缓存指的是缓存内容可被多个用户使用,如公司内部假设的Web代理;私有缓存指的是只能单独被用户使用的缓存,如浏览器缓存。


五、强制缓存基本原理以及案例?

1.对于强制缓存,浏览器不会向服务器发送任何请求,直接从本地缓存中读取文件,返回数据响应。
2.对于强制缓存,服务器响应的header中会用两个字段来表明——ExpiresCache-Control
3.HTTP1.0版本,使用的是Expires,HTTP1.1使用的是Cache-Control

Expires的字段原理案例?

Expires:
Exprires的值为服务端返回的数据到期时间。时间是相对于服务器的时间而言的,当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。

//表示该资源会在2021年7月29日11:10:23过期,过期时就会重新向服务器发送请求
Expires:Mon, 29 Jun 2021 11:10:23 GMT

但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差,另一方面,Expires是HTTP1.0的产物,故现在大多数使用Cache-Control替代。


小结:
1:强制缓存在缓存数据未失效的情况下(即Cache-Control的max-age没有过期或者Expires的缓存时间没有过期),那么就会直接使用浏览器的缓存数据,不会再向服务器发送任何请求。
2:强制缓存生效时,http状态码为200。这种方式页面的加载速度是最快的,性能也是很好的,但是在这期间,如果服务器端的资源修改了,页面上是拿不到的,因为它不会再向服务器发请求了。
3:这种情况就是我们在开发种经常遇到的,比如你修改了页面上的某个样式,在页面上刷新了但没有生效,因为走的是强缓存,所以Ctrl+ F5一顿操作之后就好了。

(此处小结参考链接:https://www.jianshu.com/p/227cee9c8d15)

Cache-Control字段原理以及案例?

Cache-Control:
为了解决expires对于服务器时间过分依赖的局限性,从HTTP1.1开始新增了cache-control字段来对expires的功能进行扩展和完善。通过对属性值max-age进行设置来控制响应资源的有效期,是一个以秒为单位的时间长度。

//以下表示该资源在被收到后的6000s内是有效的,如此可以避免服务器端和客户端时间戳不同步而造成的问题。
1:Cache-Control:max-age=6000

除了以上属性,cache-control还可以配置一些其他的属性值来更准确的控制缓存:

2:Cache-Control: no-store    

设置no-store表示进制使用任何缓存策略,禁止一切缓存,客户端的每次请求都需要服务器返回全新的响应。

3:Cache-Control: no-cache    

设置no-cache和no-store不同,两者是个相反互斥的属性,表示为强制进行协商缓存,就是每次发起的请求都不会再去判断强制缓存是否过期,而是直接与服务器协商来验证缓存的有效性,如果缓存未过期,则使用本地缓存。

//其他:
4:Cache-Control: private  //表示响应资源既可以被浏览器缓存,也可以被代理服务器缓存。
5:Cache-Control: public   //限制了响应资源只能被浏览器缓存。
6:Cache-Control: s-maxage  

指定private和public用来明确响应资源是否可以被代理服务器进行缓存。 s-maxage ,它表示缓存在代理服务器中的过期时长,且仅当设置了 public 属性值时才有效,只针对代理服务器缓存而言。


小案例:
1:利用node自身所带的http请求进行案例模拟,使用nodemon命令来进行启动服务器。
2:使用强制缓存配合Expires字段进行缓存方式的控制,进行图片加载的控制。
3:使用强制缓存配合Cache-Control字段来

可以看到下图是使用强制缓存时,同时在时间过了过期时间之后,即时间在2021-8-4 18:11:27之后不断重新刷新页面的结果,可以从控制台输出以及看到每次刷新都向服务器重新发送了请求,获取了图片的资源!

Expires字段+时间过期后:
在这里插入图片描述
下面我们更改时间来控制在资源失效时间之前进行刷新,然后进行对比两种情况的结果。

Expires字段+时间没有过期:
在这里插入图片描述

对比两图可以清楚的看到随着页面每次的重新刷新,区别在于控制台是否输出每次重新发送得请求链接,以及对于图片资源请求成功的结果是来自于服务器还是来自于本地的缓存结果!

Cache-Control字段+时间过期前后:
在上述代码基础上继续添加结构代码,设置相应的字段

else if (pathname === '/img/02.jpg') {
    const data = fs.readFileSync('./img/02.jpg')
    //强制缓存发送请求之前进行设置
    res.writeHead(200, {
      'Cache-Control': 'max-age = 5' //单位是秒
    })
    res.end(data)
  }

使用Cache-Control字段进行强制缓存控制:
在这里插入图片描述
对比属性值max-age的控制有效性前后的结果对比:
在这里插入图片描述

小结:
1:通过以上对比可以十分清晰的看到使用强制缓存的两种不同方式,对应两个不同的字段,一种发送HTTP请求,一种不需要发送。同时通过控制台输出以及浏览器工具显示可以明确的区分两种字段的设置以及带来的影响,区别在于资源的响应是否直接通过内存缓存获得,还是重发请求向服务器获取!
2:由上述分析可知,其实Cache-Control可以作为Expires的完全替代方案,并拥有其所不能具备的一些缓存控制特性,在项目中使用Cache-Control进行缓存控制就可以了!


五、协商缓存基本原理以及案例?

1.对于协商缓存,就是在使用本地缓存之前,需要新向服务器端发起一次 GET 请求,问一下服务器本地缓存是否还有效,有效就使用本地缓存即可,无效的话就把最新的资源内容发给浏览器。
2.协商缓存就是为了解决前面强制缓存资源不更新的问题。

提示:通常是采用所请求资源最近一次的修改时间戳来判断的。

last-modified字段:
1:这个字段表示的是「最后修改时间」。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。
在这里插入图片描述

2:浏览器接收到后,「如果再次请求」,在刷新一次页面,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间。
在这里插入图片描述

3:服务器拿到请求头中的If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间对比:

对比规则:
如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
否则返回304,告诉浏览器直接使用缓存。
在这里插入图片描述

4:如果此时文件资源发生了更新,我们操作图片的名称换为03.jpg,然后在改回原本的02.jpg,对应着文件资源最后修改的时间已经发生了改变,所以重新进行调用资源,对比改变。
在这里插入图片描述
可以看到,更改后第一次进行刷新页面,**最后返回的结果是200,说明请求成功的进行了发送,来获取图片资源。**如果继续刷新,则请求还是会发送,只不过后台要进行字段的判断比较,同上,还是会出现304,大家有兴趣的可以自己尝试一下!


协商缓存last-modified的缺陷?

1: 响应资源的内容没变,引起的协商缓存判断问题:
首先它只是根据资源最后的修改时间戳进行判断的,类似上文,我只是修改了图片的名称,图片的内容并没有发生改变,时间戳也会更新,从而导致协商缓存时关于有效性的判断验证为失效,需要重新进行完整的资源请求。这无疑会造成网络带宽资源的浪费,以及延长用户获取到目标资源的时间。
2:修改时间过小的问题:
其次标识文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,那么上述通过时间戳的方式来验证缓存的有效性,是无法识别出该次文件资源的更新的。

总之呢,就是服务器无法仅仅根据文件资源修改的时间戳来识别判断出真正的资源内容更新,进而导致了重新发起请求。

弥补last-modified不足,基于ETag的协商缓存?

1:ETag(Entity Tag)实体标签,是服务器根据当前文件的内容,对文件生成唯一的标识。
2:其内容主要是服务器为不同资源进行哈希运算所生成的一个字符串,该字符串类似于文件指纹,只要文件内容编码存在差异,对应的 ETag 标签值就会不同,因此可以使用 ETag 对文件资源进行更精准的变化感知

还是围绕上面的小案例进行展开:
这里注意要安装一个封装好的ETag的依赖包,使用方式逻辑和上面last-modified相同!

npm i etag    //安装这个包
//下面是安装的结果,显示相同即成功了。
PS E:\WebStorm\LeetCodePractice\HTTP缓存案例> npm i etag
npm WARN WebStorm@1.0.0 No description
npm WARN WebStorm@1.0.0 No repository field.

+ etag@1.8.1
updated 1 package in 0.9s

具体的代码我会附在最后,可以不深究后端代码,关键是理解缓存的原理和表现!
在这里插入图片描述

于是按照上面last-modified字段进行相同的判断操作,拿到这个刷新导致客户端传过来的字段进行判断即可!
在这里插入图片描述
返回304,说明ETag存在相同,即对于前端而言就会你用自己的本地缓存即可,我不会给你返回一个新的响应资源!至于文件进行了更新修改,感兴趣的可以按照上面的步骤进行尝试,更改文件名重新刷新即可!


ETag的不足缺陷?

在协商缓存中,ETag 并非 last-modified 的替代方案而是一种补充方案,因为它依旧存在一些弊端。

一方面服务器对于生成文件资源的 ETag 需要付出额外的计算开销,如果资源的尺寸较大,数量较多且修改比较频繁,那么生成 ETag 的过程就会影响服务器的性能。

另一方面 ETag字段值的生成分为强验证和弱验证,强验证根据资源内容进行生成,能够保证每个字节都相同;弱验证则根据资源的部分属性值来生成,生成速度快但无法确保每个字节都相同,所以恰当的方式是根据具体的资源使用场景选择恰当的缓存校验方式。
(参考链接:https://www.yuque.com/docs/share/51c50cef-36e2-4e40-9a6d-c4c0bcc7b2b4#VZoE0)


总结

本文是博主前端面试过程中被问到的HTTP缓存知识点引申出来的内容,由于之前了解太少,所以结合一定的小案例来加深对于HTTP缓存的理解,遂记录整理于此!接受批评指正,感谢观看!

参考文章

1:羽雀HTTP深度好文:https://www.yuque.com/docs/share/51c50cef-36e2-4e40-9a6d-c4c0bcc7b2b4#VZoE0
2:聊一聊浏览器缓存:https://juejin.cn/post/6854573215830933512#heading-40
3:一文读懂HTTP缓存:https://www.jianshu.com/p/227cee9c8d15

代码源码

//HTML片段
<body>
  <h1>HTTP 缓存</h1>
  <h2>强制缓存</h2>
  <!-- <img width="100" src="./img/01.jpg" alt="01.jpg"> -->
  <hr>
  <h2>协商缓存</h2>
  <img width="100" src="./img/02.jpg" alt="02.jpg">
</body>

//app.js片段
// 测试案例
const http = require('http')
// 为了把html页面返回回来
const fs = require('fs')
const url = require('url')
const etag = require('etag')
let serve = http.createServer((req, res) => {
  // 控制台打印输出方便对比缓存结果
  console.log(req.method, req.url)
  const { pathname } = url.parse(req.url)
  // res.end('hello world!')
  if (pathname === '/') {
    const data = fs.readFileSync('./index.html')
    res.end(data)
  } else if (pathname === '/img/01.jpg') {
    const data = fs.readFileSync('./img/01.jpg')
    //强制缓存发送请求之前进行设置
    res.writeHead(200, {
      // 响应字段 
      // 如果没有到下面设置的时间,那么缓存的资源是一直有效的。
      Expires: new Date('2021-8-5 00:21:49').toUTCString()
    })
    res.end(data)
  } else if (pathname === '/img/02.jpg') {
    const data = fs.readFileSync('./img/02.jpg')
    // 根据文件内容计算etag
    const etagContent = etag(data)

    const ifNoneMatch = req.headers['if-none-match']
    if (ifNoneMatch === etagContent) {
      res.statusCode = 304
      res.end()
      return
    }
    res.setHeader('etag', etagContent)
    // 同样需要配合下面进行协商缓存
    res.setHeader('Cache-Control', 'no-cache')
    res.end(data)
  } else {
    res.statusCode = 404
    res.end()
  }
})
serve.listen(3000, () => {
  console.log("http://localhost:3000")
})
// 协商缓存部分代码
// else if (pathname === '/img/02.jpg') {
//   // 得到文件的修改时间
//   const { mtime } = fs.statSync('./img/02.jpg')
//   const ifModifiedSince = req.headers['if-modified-since']
//   if (ifModifiedSince === mtime.toUTCString()) {
//     // 缓存生效
//     res.statusCode = 304
//     res.end()
//     return
//   }
//   const data = fs.readFileSync('./img/02.jpg')
//   //把时间发送给客户端
//   // 设置字段以及UTC时间戳,把时间发送给客户端
//   res.setHeader('last-modified', mtime.toUTCString())
//   // 如果只设置last-modefied就相当于强制缓存,就不会有协商的操作
//   res.setHeader('Cache-Control', 'no-cache')
//   res.end(data)
// } 
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值