HTTP 缓存的那些事儿

HTTP 缓存策略


阅读原文


前言

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


缓存的做用

咱们为何使用缓存,是由于缓存能够给咱们的 Web 项目带来如下好处,以提升性能和用户体验。html

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

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


缓存规则简介

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

缓存流程

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


强制缓存

一、强制缓存流程

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

强制缓存流程

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

那么如何判断缓存是否到期呢?其实仍是根据第一次访问时服务器的响应头来实现的,在 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


三、经过 Network 查看强制缓存

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

Network 缓存响应头

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

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

Network 缓存的表示

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


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

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

let 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


协商缓存

一、协商缓存流程

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

协商缓存流程

协商缓存和强制缓存不一样的是,协商缓存每次请求都须要跟服务器通讯,并且命中缓存服务器返回状态码再也不是 200,而是 304

二、协商缓存判断标识

强制缓存是经过过时时间来控制是否访问服务器,而协商缓存每次都要与服务器交互对比缓存标识,一样的,对于协商缓存的实如今 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

三、经过 Network 查看协商缓存

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

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

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

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

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

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

第一次请求:

Network 第一次请求

缓存后请求:

Network 缓存后请求

经过两图的对比,咱们能够发现,协商缓存生效时的状态码为 304,而且报文大小和请求时间大大减小,缘由是服务端在进行标识比对后只返回了 header 部分,经过状态码来通知浏览器使用缓存,再也不须要将报文主体部分一块儿返回给浏览器。

四、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");

let 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 状态码时,浏览器直接使用缓存,若是协商缓存也未命中,则服务器从新设置协商缓存的标识。

相关文章
相关标签/搜索