浅谈HTTP 缓存的那些事儿


Posted in Javascript onOctober 17, 2018

前言

HTTP 缓存机制作为 Web 应用性能优化的重要手段,对于从事 Web 开发的同学们来说,应该是知识体系的基础环节,也是想要成为前端架构的必备技能。

缓存的作用

我们为什么使用缓存,是因为缓存可以给我们的 Web 项目带来以下好处,以提高性能和用户体验。

  • 加快了浏览器加载网页的速度;
  • 减少了冗余的数据传输,节省网络流量和带宽;
  • 减少服务器的负担,大大提高了网站的性能。

由于从本地缓存读取静态资源,加快浏览器的网页加载速度是一定的,也确实的减少了数据传输,就提高网站性能来说,可能一两个用户的访问对于减小服务器的负担没有明显效果,但如果这个网站在高并发的情况下,使用缓存对于减小服务器压力和整个网站的性能都会发生质的变化。

缓存规则简介

为了方便理解,我们认为浏览器存在一个缓存数据库,用于存储缓存信息(实际上静态资源是被缓存到了内存和磁盘中),在浏览器第一次请求数据时,此时缓存数据库没有对应的缓存数据,则需要请求服务器,服务器会将缓存规则和数据返回,浏览器将缓存规则和数据存储进缓存数据库。

浅谈HTTP 缓存的那些事儿

当浏览器地址栏输入地址后请求的 index.html 是不会被缓存的,但 index.html 内部请求的其他资源会遵循缓存策略,HTTP 缓存有多种规则,根据是否需要向服务器发送请求主要分为两大类,强制缓存和协商缓存。

强制缓存

1、强制缓存流程

强制缓存是第一次访问服务器获取数据后,在有效时间内不会再请求服务器,而是直接使用缓存数据,强制缓存的流程如下。

浅谈HTTP 缓存的那些事儿

2、强制缓存判断到期时间

那么如何判断缓存是否到期呢?其实还是根据第一次访问时服务器的响应头来实现的,在 HTTP 1.0 版本和 HTTP 1.1 版本有所不同。

HTTP 1.0 版本,服务器使用的响应头字段为 Expires,值为未来的绝对时间(时间戳),浏览器请求时的当前时间超过了 Expires 设置的时间,代表缓存失效,需要再次向服务器发送请求,否则都会直接从缓存数据库中获取数据。

HTTP 1.1 版本,服务器使用的响应头字段为 Cache-Control,有多个值,意义各不相同。

  • private:客户端可以缓存;
  • public:客户端和代理服务器都可以缓存(对于前端而言,可以认为与 private 效果相同);
  • max-age=xxx:缓存的内容将在 xxx 秒后过期(相对时间,秒为单位);
  • no-cache:需要使用协商缓存(后面介绍)来验证数据是否过期;
  • no-store:所有内容都不会缓存,强制缓存和协商缓存都不会触发。

Cache-Control 的值中最常用的为 max-age=xxx,缓存本身就是为了数据传输的优化和性能而存在的,所以 no-store 几乎不会使用。

注意:在 HTTP 1.0 版本中,Expires 字段的绝对时间是从服务器获取的,由于请求需要时间,所以浏览器的请求时间与服务器接收到请求所获取的时间是存在误差的,这也导致了缓存命中的误差,在 HTTP 1.1 版本中,因为 Cache-Control 的值 max-age=xxx 中的 xxx 是以秒为单位的相对时间,所以在浏览器接收到资源后开始倒计时,规避了 HTTP 1.0 中缓存命中存在误差的缺点,为了兼容低版本 HTTP 协议,正常开发中两种响应头会同时使用,HTTP 1.1 版本的实现优先级高于 HTTP 1.0

3、通过 Network 查看强制缓存

我们通过 Chrome 浏览器的开发者工具,打开 NetWork 查看强制缓存的相关信息。

浅谈HTTP 缓存的那些事儿

上面是百度网站 Logo 图片的响应,我们可以清楚的看到,其中兼容了 HTTP 1.0HTTP 1.1 版本,并使用强制缓存存储了 10 年。

下面看一看通过缓存取出的数据在 Network 中与其他资源的区别。

浅谈HTTP 缓存的那些事儿

其实缓存的储存是内存和磁盘两个位置,由当前浏览器本身的策略决定,比较随机,从内存的缓存中取出的数据会显示 (from memory cache),从磁盘的缓存中取出的数据会显示 (from disk cache)

4、NodeJS 服务器实现强制缓存

// 强制缓存
const http = require("http");
const url = require("url");
const path = require("path");
const mime = require("mime");
const fs = require("fs");

const server = http.createServer((req, res) => {
 let { pathname } = url.parse(req.url, true);
 pathname = pathname !== "/" ? pathname : "/index.html";

 // 获取读取文件的绝对路径
 let p = path.join(__dirname, pathname);

 // 查看路径是否合法
 fs.access(p, err => {
  // 路径不合法则直接中断连接
  if (err) return res.end("Not Found");

  // 设置强制缓存
  res.setHeader("Expires", new Date(Date.now() + 30000).toGMTString());
  res.setHeader("Cache-Control", "max-age=30");

  // 设置文件类型并响应给浏览器
  res.setHeader("Content-Type", `${mime.getType(p)};charset=utf8`);
  fs.createReadStream(p).pipe(res);
 });
});

server.listen(3000, () => {
 console.log("server start 3000");
});

上面 mime 模块的 getType 方法可以成功返回传入路径下文件对应的文件类型,如 text/htmlapplication/javascript 等,是第三方模块,使用之前需要安装。

npm install mime

协商缓存

1、协商缓存流程

协商缓存又叫对比缓存,设置协商缓存后,第一次访问服务器获取数据时,服务器会将数据和缓存标识一起返回给浏览器,客户端会将数据和标识存入缓存数据库中,下一次请求时,会先去缓存中取出缓存标识发送给服务器进行询问,当服务器数据更改时会更新标识,所以服务器拿到浏览器发来的标识进行对比,相同代表数据未更改,响应浏览器通知数据未更改,浏览器会去缓存中获取数据,如果标识不同,代表服务器更改过数据,所以会将新的数据和新的标识返回浏览器,浏览器会将新的数据和标识存入缓存中,协商缓存的流程如下。

浅谈HTTP 缓存的那些事儿

协商缓存和强制缓存不同的是,协商缓存每次请求都需要跟服务器通信,而且命中缓存服务器返回状态码不再是 200,而是 304

2、协商缓存判断标识

强制缓存是通过过期时间来控制是否访问服务器,而协商缓存每次都要与服务器交互对比缓存标识,同样的,对于协商缓存的实现在 HTTP 1.0 版本和 HTTP 1.1 版本也有所不同。

HTTP 1.0 版本中,服务器通过 Last-Modified 响应头来设置缓存标识,通常取请求数据的最后修改时间(绝对时间)作为值,而浏览器将接收到返回的数据和标识存入缓存,再次请求会自动发送 If-Modified-Since 请求头,值为之前返回的最后修改时间(标识),服务器取出 If-Modified-Since 的值与数据的上次修改时间对比,如果上次修改时间大于了 If-Modified-Since 的值,说明被修改过,则通过 Last-Modified 响应头返回新的最后修改时间和新的数据,否则未被修改,返回状态码 304 通知浏览器命中缓存。

HTTP 1.1 版本中,服务器通过 Etag 响应头来设置缓存标识(唯一标识,像一个指纹一样,生成规则由服务器来决定),浏览器接收到数据和唯一标识后存入缓存,下次请求时,通过 If-None-Match 请求头将唯一标识带给服务器,服务器取出唯一标识与之前的标识对比,不同,说明修改过,返回新标识和数据,相同,则返回状态码 304 通知浏览器命中缓存。

HTTP 协商缓存策略流程图如下:

浅谈HTTP 缓存的那些事儿

注意:使用协商缓存时 HTTP 1.0 版本还是不太靠谱,假设一个文件增加了一个字符后又删除了,文件相当于没更改,但是最后修改时间变了,会被当作修改处理,本应该命中缓存,服务器却重新发送了数据,因此 HTTP 1.1 中使用的 Etag 唯一标识是根据文件内容或摘要生成的,保证了只要文件内容不变,则一定会命中缓存,为了兼容低版本 HTTP 协议,开发中两种响应头也会同时使用,同样 HTTP 1.1 版本的实现优先级高于 HTTP 1.0

3、通过 Network 查看协商缓存

我们同样通过 Chrome 浏览器的开发者工具,打开 NetWork 查看协商缓存的相关信息。

再次请求服务器的请求头信息:

浅谈HTTP 缓存的那些事儿

命中协商缓存的响应头信息:

浅谈HTTP 缓存的那些事儿

下面看一看通过协商缓存取出的数据在 Network 中与第一次加载的区别。

第一次请求:

浅谈HTTP 缓存的那些事儿

缓存后请求:

浅谈HTTP 缓存的那些事儿

通过两图的对比,我们可以发现,协商缓存生效时的状态码为 304,并且报文大小和请求时间大大减少,原因是服务端在进行标识比对后只返回了 header 部分,通过状态码来通知浏览器使用缓存,不再需要将报文主体部分一起返回给浏览器。

4、NodeJS 服务器实现协商缓存

// 协商缓存
const http = require("http");
const url = require("url");
const path = require("path");
const mime = require("mime");
const fs = require("fs");0
const crytpo = require("crytpo");

const server = http.createServer((req, res) => {
 let { pathname } = url.parse(req.url, true);
 pathname = pathname !== "/" ? pathname : "/index.html";

 // 获取读取文件的绝对路径
 let p = path.join(__dirname, pathname);

 // 查看路径是否合法
 fs.stat(p, (err, statObj) => {
  // 路径不合法则直接中断连接
  if (err) return res.end("Not Found");

  let md5 = crypto.createHash("md5"); // 创建加密的转换流
  let rs = fs.createReadStream(p); // 创建可读流

  // 读取文件内容并加密
  rs.on("data", data => md5.update(data));

  rs.on("end", () => {
   let ctime = statObj.ctime.toGMTString(); // 获取文件最后修改时间
   let flag = md5.digest("hex"); // 获取加密后的唯一标识

   // 获取协商缓存的请求头
   let ifModifiedSince = req.headers["if-modified-since"];
   let ifNoneMatch = req.headers["if-none-match"];

   if (ifModifiedSince === ctime || ifNoneMatch === flag) {
    res.statusCode = 304;
    res.end();
   } else {
    // 设置协商缓存
    res.setHeader("Last-Modified", ctime);
    res.setHeader("Etag", flag);

    // 设置文件类型并响应给浏览器
    res.setHeader("Content-Type", `${mime.getType(p)};charset=utf8`);
    rs.pipe(res);
   }
  });
 });
});

server.listen(3000, () => {
 console.log("server start 3000");
});

在上面的代码中是通过可读流读取文件内容,并通过 crypto 模块进行了 md5 加密后的结果作为了唯一标识,这样就能保证只要文件内容不变,就会命中缓存,其中兼容了 HTTP 1.0HTTP 1.1 两个版本,只要满足一个则直接返回 304 通知浏览器命中缓存。

注意:其实读取文件内容加密这种做法并不可取,假如读取的是大文件,在读取文件内容和进行 md5 加密这个过程会非常消耗时间,所以在开发中要针对业务的实际情况选择可以保证服务器性能的方式生成唯一标识,比如根据文件的摘要。

总结

为了使缓存策略更加健壮、灵活,HTTP 1.0 版本 和 HTTP 1.1 版本的缓存策略会同时使用,甚至强制缓存和协商缓存也会同时使用,对于强制缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接使用缓存,超出有效时间,执行协商缓存策略,对于协商缓存,将缓存信息中的 EtagLast-Modified 通过请求头 If-None-MatchIf-Modified-Since 发送给服务器,由服务器校验同时设置新的强制缓存,校验通过并返回 304 状态码时,浏览器直接使用缓存,如果协商缓存也未命中,则服务器重新设置协商缓存的标识。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript 字符串与数组转换函数[不用split与join]
Dec 13 Javascript
心扬JS分页函数代码
Sep 10 Javascript
等待指定时间后自动跳转或关闭当前页面的js代码
Jul 09 Javascript
jquery的flexigrid无法显示数据提示获取到数据
Jul 19 Javascript
js动态添加表格数据使用insertRow和insertCell实现
May 22 Javascript
Javascript学习笔记之函数篇(六) : 作用域与命名空间
Nov 23 Javascript
jQuery实现平滑滚动到指定锚点的方法
Mar 20 Javascript
BootStrap下jQuery自动完成的样式调整
May 30 Javascript
js序列化和反序列化的使用讲解
Jan 19 Javascript
angular 表单验证器验证的同时限制输入的实现
Apr 11 Javascript
Vue3.0 响应式系统源码逐行分析讲解
Oct 14 Javascript
React中使用Vditor自定义图片详解
Dec 25 Javascript
使用angular-cli webpack创建多个包的方法
Oct 16 #Javascript
element-ui的回调函数Events的用法详解
Oct 16 #Javascript
基于vue-upload-component封装一个图片上传组件的示例
Oct 16 #Javascript
Postman的下载及安装教程详解
Oct 16 #Javascript
Vue.js 时间转换代码及时间戳转时间字符串
Oct 16 #Javascript
详解angular2如何手动点击特定元素上的点击事件
Oct 16 #Javascript
iView框架问题整理小结
Oct 16 #Javascript
You might like
一个图片地址分解程序(用于PHP小偷程序)
2014/08/23 PHP
WordPress中给媒体文件添加分类和标签的PHP功能实现
2015/12/31 PHP
jQuery下扩展插件和拓展函数的写法(匿名函数使用的典型例子)
2010/10/20 Javascript
jQuery图片轮播的具体实现
2013/09/11 Javascript
JS获取select的value和text值的简单实例
2014/02/26 Javascript
jQuery实现鼠标经过时出现隐藏层文字链接的方法
2015/10/12 Javascript
jQuery四种选择器使用及示例
2016/06/05 Javascript
微信小程序 wxapp导航 navigator详解
2016/10/31 Javascript
JavaScript实现经典排序算法之冒泡排序
2016/12/28 Javascript
js实现五星评价功能
2017/03/08 Javascript
js实现canvas保存图片为png格式并下载到本地的方法
2017/08/31 Javascript
vue 挂载路由到头部导航的方法
2017/11/13 Javascript
webpack4 SplitChunks实现代码分隔详解
2019/05/23 Javascript
layer弹窗在键盘按回车将反复刷新的实现方法
2019/09/25 Javascript
在博客园博文中添加自定义右键菜单的方法详解
2020/02/05 Javascript
[38:32]完美世界DOTA2联赛循环赛 Forest vs DM 第二场 11.06
2020/11/06 DOTA
利用Python开发微信支付的注意事项
2016/08/19 Python
Windows安装Python、pip、easy_install的方法
2017/03/05 Python
Python二叉树的镜像转换实现方法示例
2019/03/06 Python
python 判断三个数字中的最大值实例代码
2019/07/24 Python
django 数据库 get_or_create函数返回值是tuple的问题
2020/05/15 Python
使用opencv中匹配点对的坐标提取方式
2020/06/04 Python
全球酒店预订网站:Hotels.com
2016/08/10 全球购物
.NET笔试题(20个问题)
2016/02/02 面试题
通息工程毕业生自荐信
2013/10/16 职场文书
公司部门司机岗位职责
2014/01/03 职场文书
教育科研先进个人材料
2014/01/26 职场文书
毕业生大学生活自我总结
2014/01/31 职场文书
中介公司区域经理岗位职责范本
2014/03/02 职场文书
植树节口号
2014/06/21 职场文书
2014年人民调解工作总结
2014/12/08 职场文书
计生个人工作总结
2015/02/28 职场文书
单独二胎证明
2015/06/24 职场文书
教育教学工作反思
2016/02/24 职场文书
深入浅析React中diff算法
2021/05/19 Javascript
使用compose函数优化代码提高可读性及扩展性
2022/06/16 Javascript