浅谈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 相关文章推荐
JQuery 自定义CircleAnimation,Animate方法学习笔记
Jul 10 Javascript
TextArea设置MaxLength属性最大输入值的js代码
Dec 21 Javascript
IE6已终止操作问题的2种情况及解决
Apr 23 Javascript
一个不错的仿携程自定义数据下拉选择select
Sep 01 Javascript
JavaScript strike方法入门实例(给字符串加上删除线)
Oct 17 Javascript
AngularJS入门教程(二):AngularJS模板
Dec 06 Javascript
45个JavaScript编程注意事项、技巧大全
Feb 11 Javascript
jQuery实现的超酷苹果风格图标滑出菜单效果代码
Sep 16 Javascript
纯jQuery实现前端分页功能
Mar 23 jQuery
详解webpack+vue-cli项目打包技巧
Jun 17 Javascript
vue使用技巧及vue项目中遇到的问题
Jun 04 Javascript
微信小程序实现聊天对话(文本、图片)功能
Jul 06 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中strtr字符串替换用法详解
2014/11/26 PHP
php打印一个边长为N的实心和空心菱型的方法
2015/03/02 PHP
twig模板常用语句实例小结
2016/02/04 PHP
PHP实现的统计数据功能详解
2016/12/06 PHP
js字符串转换成xml对象并使用技巧解读
2013/04/18 Javascript
Jquery中children与find之间的区别详细解析
2013/11/29 Javascript
后台获取ZTREE选中节点的方法
2015/02/12 Javascript
jquery插件jSignature实现手动签名
2015/05/04 Javascript
详细解读JavaScript的跨浏览器事件处理
2015/08/12 Javascript
onclick和onblur冲突问题的快速解决方法
2016/04/28 Javascript
javascript实现起伏的水波背景效果
2016/05/16 Javascript
vue弹窗组件使用方法
2018/04/28 Javascript
webpack 样式加载的实现原理
2018/06/12 Javascript
详解NodeJs项目 CentOs linux服务器线上部署
2019/09/16 NodeJs
微信小程序实现点击图片放大预览
2019/10/21 Javascript
vue 获取元素额外生成的data-v-xxx操作
2020/09/09 Javascript
python实现可将字符转换成大写的tcp服务器实例
2015/04/29 Python
深入学习Python中的上下文管理器与else块
2017/08/27 Python
Python基于回溯法子集树模板解决选排问题示例
2017/09/07 Python
利用python求解物理学中的双弹簧质能系统详解
2017/09/29 Python
详解Django rest_framework实现RESTful API
2018/05/24 Python
Python实现的多叉树寻找最短路径算法示例
2018/07/30 Python
Flask-WTF表单的使用方法
2019/07/12 Python
解决pytorch 保存模型遇到的问题
2021/03/03 Python
HTML5实现直播间评论滚动效果的代码
2020/05/27 HTML / CSS
卡塔尔航空官方网站:Qatar Airways
2017/02/08 全球购物
凯撒娱乐:Caesars Entertainment
2018/02/23 全球购物
德国隐形眼镜店:LuckyLens
2018/07/29 全球购物
附答案的Java面试题
2012/11/19 面试题
师范学院毕业生求职信范文
2013/12/26 职场文书
就业推荐表自我鉴定
2014/03/21 职场文书
公司离职证明标准样本
2014/10/05 职场文书
好人好事新闻稿
2015/07/17 职场文书
毕业设计工作总结
2015/08/14 职场文书
MongoDB安装使用并实现Python操作数据库
2021/06/28 MongoDB
OpenCV项目实践之停车场车位实时检测
2022/04/11 Python