前言
HTTP 缓存机制作为 Web 应用性能优化的重要手段,对于从事 Web 开发的同学们来说,应该是知识体系的基础环节,也是想要成为前端架构的必备技能。
缓存的作用
我们为什么使用缓存,是因为缓存可以给我们的 Web 项目带来以下好处,以提高性能和用户体验。
由于从本地缓存读取静态资源,加快浏览器的网页加载速度是一定的,也确实的减少了数据传输,就提高网站性能来说,可能一两个用户的访问对于减小服务器的负担没有明显效果,但如果这个网站在高并发的情况下,使用缓存对于减小服务器压力和整个网站的性能都会发生质的变化。
缓存规则简介
为了方便理解,我们认为浏览器存在一个缓存数据库,用于存储缓存信息(实际上静态资源是被缓存到了内存和磁盘中),在浏览器第一次请求数据时,此时缓存数据库没有对应的缓存数据,则需要请求服务器,服务器会将缓存规则和数据返回,浏览器将缓存规则和数据存储进缓存数据库。
当浏览器地址栏输入地址后请求的 index.html 是不会被缓存的,但 index.html 内部请求的其他资源会遵循缓存策略,HTTP 缓存有多种规则,根据是否需要向服务器发送请求主要分为两大类,强制缓存和协商缓存。
强制缓存
1、强制缓存流程
强制缓存是第一次访问服务器获取数据后,在有效时间内不会再请求服务器,而是直接使用缓存数据,强制缓存的流程如下。
2、强制缓存判断到期时间
那么如何判断缓存是否到期呢?其实还是根据第一次访问时服务器的响应头来实现的,在 HTTP 1.0 版本和 HTTP 1.1 版本有所不同。
在 HTTP 1.0 版本,服务器使用的响应头字段为 Expires,值为未来的绝对时间(时间戳),浏览器请求时的当前时间超过了 Expires 设置的时间,代表缓存失效,需要再次向服务器发送请求,否则都会直接从缓存数据库中获取数据。
在 HTTP 1.1 版本,服务器使用的响应头字段为 Cache-Control,有多个值,意义各不相同。
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 查看强制缓存的相关信息。
上面是百度网站 Logo 图片的响应,我们可以清楚的看到,其中兼容了 HTTP 1.0 和 HTTP 1.1 版本,并使用强制缓存存储了 10 年。
下面看一看通过缓存取出的数据在 Network 中与其他资源的区别。
其实缓存的储存是内存和磁盘两个位置,由当前浏览器本身的策略决定,比较随机,从内存的缓存中取出的数据会显示 (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/html 和 application/javascript 等,是第三方模块,使用之前需要安装。
npm install mime
协商缓存
1、协商缓存流程
协商缓存又叫对比缓存,设置协商缓存后,第一次访问服务器获取数据时,服务器会将数据和缓存标识一起返回给浏览器,客户端会将数据和标识存入缓存数据库中,下一次请求时,会先去缓存中取出缓存标识发送给服务器进行询问,当服务器数据更改时会更新标识,所以服务器拿到浏览器发来的标识进行对比,相同代表数据未更改,响应浏览器通知数据未更改,浏览器会去缓存中获取数据,如果标识不同,代表服务器更改过数据,所以会将新的数据和新的标识返回浏览器,浏览器会将新的数据和标识存入缓存中,协商缓存的流程如下。
协商缓存和强制缓存不同的是,协商缓存每次请求都需要跟服务器通信,而且命中缓存服务器返回状态码不再是 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 1.0 版本还是不太靠谱,假设一个文件增加了一个字符后又删除了,文件相当于没更改,但是最后修改时间变了,会被当作修改处理,本应该命中缓存,服务器却重新发送了数据,因此 HTTP 1.1 中使用的 Etag 唯一标识是根据文件内容或摘要生成的,保证了只要文件内容不变,则一定会命中缓存,为了兼容低版本 HTTP 协议,开发中两种响应头也会同时使用,同样 HTTP 1.1 版本的实现优先级高于 HTTP 1.0。
3、通过 Network 查看协商缓存
我们同样通过 Chrome 浏览器的开发者工具,打开 NetWork 查看协商缓存的相关信息。
再次请求服务器的请求头信息:
命中协商缓存的响应头信息:
下面看一看通过协商缓存取出的数据在 Network 中与第一次加载的区别。
第一次请求:
缓存后请求:
通过两图的对比,我们可以发现,协商缓存生效时的状态码为 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.0 和 HTTP 1.1 两个版本,只要满足一个则直接返回 304 通知浏览器命中缓存。
注意:其实读取文件内容加密这种做法并不可取,假如读取的是大文件,在读取文件内容和进行 md5 加密这个过程会非常消耗时间,所以在开发中要针对业务的实际情况选择可以保证服务器性能的方式生成唯一标识,比如根据文件的摘要。
总结
为了使缓存策略更加健壮、灵活,HTTP 1.0 版本 和 HTTP 1.1 版本的缓存策略会同时使用,甚至强制缓存和协商缓存也会同时使用,对于强制缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接使用缓存,超出有效时间,执行协商缓存策略,对于协商缓存,将缓存信息中的 Etag 和 Last-Modified 通过请求头 If-None-Match 和 If-Modified-Since 发送给服务器,由服务器校验同时设置新的强制缓存,校验通过并返回 304 状态码时,浏览器直接使用缓存,如果协商缓存也未命中,则服务器重新设置协商缓存的标识。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持小牛知识库。
本文向大家介绍浅谈Vue服务端渲染框架Nuxt的那些事,包括了浅谈Vue服务端渲染框架Nuxt的那些事的使用技巧和注意事项,需要的朋友参考一下 Vue因其简单易懂的API、高效的数据绑定和灵活的组件系统,受到很多前端开发人员的青睐。国内很多公司都在使用vue进行项目开发,我们正在使用的简书,便是基于Vue来构建的。 我们知道,SPA前端渲染存在两大痛点:(1)SEO。搜索引擎爬虫难以抓取客户端渲染
本文向大家介绍浅谈Django的缓存机制,包括了浅谈Django的缓存机制的使用技巧和注意事项,需要的朋友参考一下 由于Django是动态网站,所有每次请求均会去数据进行相应的操作,当程序访问量大时,耗时必然会更加明显,最简单解决方式是使用:缓存,缓存将一个某个views的返回值保存至内存或者memcache中,5分钟内再有人来访问时,则不再去执行view中的操作,而是直接从内存或者Redis中之
本文向大家介绍浅谈PHP中的那些魔术常量,包括了浅谈PHP中的那些魔术常量的使用技巧和注意事项,需要的朋友参考一下 之前我们已经了解了一些常用的魔术方法,除了魔术方法外,PHP还提供一些魔术常量,相信大家在日常的工作中也都使用过,这里给大家做一个总结。 其实PHP还提供了很多常量但都依赖于各类扩展库,而有几个常量是通用并且是跟随它们所在代码的位置来提供一些与位置有关的信息,这些就是魔术常量。魔术常
本文向大家介绍浅谈iOS 屏幕方向那点事儿,包括了浅谈iOS 屏幕方向那点事儿的使用技巧和注意事项,需要的朋友参考一下 一般的应用,只会支持竖屏正方向一个方向,支持多个屏幕方向的应用还是比较少的。 不过我在工作的项目中,跟这个屏幕方向接触比较多,因为我们是一个有界面的 SDK,要让接入方接入的,一开始做没什么经验,考虑到接入方本身的屏幕方向可能是多种的,所以我们直接上来就支持四个方向,然后就是各种
本文向大家介绍浅谈Glide缓存key的问题,包括了浅谈Glide缓存key的问题的使用技巧和注意事项,需要的朋友参考一下 最近项目里面有个地方是在前面用glide加载图片后,后面再另外一个地方加载相同图片时没有复用glide的缓存,而是自己另外又重新缓存了一套。 查找后发现问题是glide缓存的key不一致的问题。 从key的生成可以看到和很多参数有关,逐一排查后,发现了width和height
本文向大家介绍浅谈iOS UIWebView对H5的缓存功能,包括了浅谈iOS UIWebView对H5的缓存功能的使用技巧和注意事项,需要的朋友参考一下 这两天在搞与H5交互的事,之前做的都是加载的静态的web页面,交互调试起来很快,这次搞的是js写的前端页面,跳转什么的都是动态的,然后就不响应了,搞了半天原来是缓存的问题,这里简单介绍一下,一般请求会使用下面的方法: 该方法的描述如下: Cre