浅谈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 相关文章推荐
js 设置选中行的样式的实现代码
May 24 Javascript
JavaScript 的继承
Oct 01 Javascript
用JS在浏览器中创建下载文件
Mar 05 Javascript
重写document.write实现无阻塞加载js广告(补充)
Dec 12 Javascript
js中的内部属性与delete操作符介绍
Aug 10 Javascript
js实现获取div坐标的方法
Nov 16 Javascript
写jQuery插件时的注意点
Feb 20 Javascript
jQuery返回定位插件详解
May 15 jQuery
vue this.reload 方法 配置
Sep 12 Javascript
ES6 更易于继承的类语法的使用
Feb 11 Javascript
koa+jwt实现token验证与刷新功能
May 30 Javascript
使用Promise封装小程序wx.request的实现方法
Nov 13 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
谈一谈收音机的高放电路
2021/03/02 无线电
咖啡风味 世界咖啡主要分布分布 咖啡的生长要求
2021/03/06 新手入门
解析php中const与define的应用区别
2013/06/18 PHP
PHP中使用Imagick操作PSD文件实例
2015/01/26 PHP
PHP获取一年中每个星期的开始和结束日期的方法
2015/02/12 PHP
深入讲解PHP的Yii框架中的属性(Property)
2016/03/18 PHP
php+ajax实现异步上传文件或图片功能
2017/07/18 PHP
Laravel框架表单验证操作实例分析
2019/09/30 PHP
简单JS代码压缩器
2006/10/12 Javascript
Lazy Load 延迟加载图片的 jQuery 插件
2010/02/06 Javascript
jquery调整表格行tr上下顺序实例讲解
2016/01/09 Javascript
使用postMesssage()实现iframe跨域页面间的信息传递
2016/03/29 Javascript
简单解析JavaScript中的__proto__属性
2016/05/10 Javascript
jQuery实现图片轮播效果代码
2016/09/27 Javascript
基于JavaScript实现百度搜索框效果
2020/06/28 Javascript
浅谈vue-cli加载不到dev-server.js的解决办法
2017/11/24 Javascript
微信小程序实现左右联动的实战记录
2018/07/05 Javascript
react MPA 多页配置详解
2019/10/18 Javascript
vue随机验证码组件的封装实现
2020/02/19 Javascript
Python基于递归算法实现的走迷宫问题
2017/08/04 Python
[原创]pip和pygal的安装实例教程
2017/12/07 Python
对Python 2.7 pandas 中的read_excel详解
2018/05/04 Python
浅谈Python里面小数点精度的控制
2018/07/16 Python
使用python实现kNN分类算法
2019/10/16 Python
python 多进程队列数据处理详解
2019/12/23 Python
浅析Django中关于session的使用
2019/12/30 Python
Python调用系统命令os.system()和os.popen()的实现
2020/12/31 Python
Expedia泰国:预订机票、酒店和旅游包(航班+酒店)
2016/09/27 全球购物
JINS眼镜官方网站:日本最大的眼镜邮购
2016/10/14 全球购物
巴西网上药店:Drogaria Araujo
2021/01/06 全球购物
摄影专业毕业生求职信
2014/03/13 职场文书
疾病防治方案
2014/05/31 职场文书
2014年师德师风自我剖析材料
2014/09/27 职场文书
诉讼代理人授权委托书
2014/10/11 职场文书
普通党员群众路线教育实践活动心得体会
2014/11/04 职场文书
2015年国培研修感言
2015/08/01 职场文书