理解http浏览器的协商缓存和强制缓存

阅读目录css

一:浏览器缓存的做用是什么?html

1. 缓存能够减小冗余的数据传输。节省了网络带宽,从而更快的加载页面。
2. 缓存下降了服务器的要求,从而服务器更快的响应。node

那么咱们使用缓存,缓存的资源文件到什么地方去了呢?git

那么首先来看下 memory cache 和 disk cache 缓存github

memory cache: 它是将资源文件缓存到内存中。等下次请求访问的时候不须要从新下载资源,而是直接从内存中读取数据。算法

disk cache: 它是将资源文件缓存到硬盘中。等下次请求的时候它是直接从硬盘中读取。浏览器

那么他们两则的区别是?缓存

memory cache(内存缓存)退出进程时数据会被清除,而disk cache(硬盘缓存)退出进程时数据不会被清除。内存读取比硬盘中读取的速度更快。可是咱们也不能把全部数据放在内存中缓存的,由于内存也是有限的。服务器

memory cache(内存缓存)通常会将脚本、字体、图片会存储到内存缓存中。
disk cache(硬盘缓存) 通常非脚本会存放在硬盘中,好比css这些。网络

缓存读取的原理:先从内存中查找对应的缓存,若是内存中能找到就读取对应的缓存,不然的话就从硬盘中查找对应的缓存,若是有就读取,不然的话,就从新网络请求。

那么浏览器缓存它又分为2种:强制缓存和协商缓存。

协商缓存原理:客户端向服务器端发出请求,服务端会检测是否有对应的标识,若是没有对应的标识,服务器端会返回一个对应的标识给客户端,客户端下次再次请求的时候,把该标识带过去,而后服务器端会验证该标识,若是验证经过了,则会响应304,告诉浏览器读取缓存。若是标识没有经过,则返回请求的资源。

那么协商缓存的标识又有2种:ETag/if-None-Match 和 Last-Modified/if-Modify-Since

协商缓存Last-Modified/if-Modify-Since

浏览器第一次发出请求一个资源的时候,服务器会返回一个last-Modify到hearer中. Last-Modify 含义是最后的修改时间。
当浏览器再次请求的时候,request的请求头会加上 if-Modify-Since,该值为缓存以前返回的 Last-Modify. 服务器收到if-Modify-Since后,根据资源的最后修改时间(last-Modify)和该值(if-Modify-Since)进行比较,若是相等的话,则命中缓存,返回304,不然, 若是 Last-Modify > if-Modify-Since, 则会给出200响应,而且更新Last-Modify为新的值。

下面咱们使用node来模拟下该场景。基本的代码以下:

import Koa from 'koa';
import path from 'path';

//静态资源中间件
import resource from 'koa-static';
const app = new Koa();
const host = 'localhost';
const port = 7788;

const url = require('url');
const fs = require('fs');
const mime = require('mime');

app.use(async(ctx, next) => {
  // 获取文件名
  const { pathname } = url.parse(ctx.url, true);
  // 获取文件路径
  const filepath = path.join(__dirname, pathname);
  const req = ctx.req;
  const res = ctx.res;
  // 判断文件是否存在
  fs.stat(filepath, (err, stat) => {
    if (err) {
      res.end('not found');
    } else {
      // 获取 if-modified-since 这个请求头
      const ifModifiedSince = req.headers['if-modified-since'];
      // 获取最后修改的时间
      const lastModified = stat.ctime.toGMTString();
      // 判断二者是否相等,若是相等返回304读取浏览器缓存。不然的话,从新发请求
      if (ifModifiedSince === lastModified) {
        res.writeHead(304);
        res.end();
      } else {
        res.setHeader('Content-Type', mime.getType(filepath));
        res.setHeader('Last-Modified', stat.ctime.toGMTString());
        // fs.createReadStream(filepath).pipe(res);
      }
    }
  });
  await next();
});

app.use(resource(path.join(__dirname, './static')));

app.listen(port, () => {
  console.log(`server is listen in ${host}:${port}`);
});

当咱们第一次访问的时候(清除浏览器的缓存),以下图所示:

当咱们继续刷新浏览器的时候,咱们再看下以下数据:

如上能够看到,当咱们第二次请求的时候,请求头部加上了 If-Modified-Since 该参数,而且该参数的值该响应头中的时间相同。所以返回304状态。
查看demo,请看github上的源码

协商缓存ETag/if-None-Match

ETag的原理和上面的last-modified是相似的。ETag则是对当前请求的资源作一个惟一的标识。该标识能够是一个字符串,文件的size,hash等。只要可以合理标识资源的惟一性并能验证是否修改过就能够了。ETag在服务器响应请求的时候,返回当前资源的惟一标识(它是由服务器生成的)。可是只要资源有变化,ETag会从新生成的。浏览器再下一次加载的时候会向服务器发送请求,会将上一次返回的ETag值放到request header 里的 if-None-Match里面去,服务器端只要比较客户端传来的if-None-Match值是否和本身服务器上的ETag是否一致,若是一致说明资源未修改过,所以返回304,若是不一致,说明修改过,所以返回200。而且把新的Etag赋值给if-None-Match来更新该值。

last-modified 和 ETag之间对比

1. 在精度上,ETag要优先于 last-modified。
2. 在性能上,Etag要逊于Last-Modified,Last-Modified须要记录时间,而Etag须要服务器经过算法来计算出一个hash值。
3. 在优先级上,服务器校验优先考虑Etag。

下面咱们继续使用node来演示下:基本代码以下:

import path from 'path';
import Koa from 'koa';

//静态资源中间件
import resource from 'koa-static';
const app = new Koa();
const host = 'localhost';
const port = 7878;

const url = require('url');
const fs = require('fs');
const mime = require('mime');
/*
const crypto = require('crypto');
app.use(async(ctx, next) => {
  // 获取文件名
  const { pathname } = url.parse(ctx.url, true);
  // 获取文件路径
  const filepath = path.join(__dirname, pathname);
  const req = ctx.req;
  const res = ctx.res;
  // 判断文件是否存在
  fs.stat(filepath, (err, stat) => {
    if (err) {
      res.end('not found');
    } else {
      console.log(111);
      // 获取 if-none-match 这个请求头
      const ifNoneMatch = req.headers['if-none-match'];
      const readStream = fs.createReadStream(filepath);
      const md5 = crypto.createHash('md5');
      // 经过流的方式读取文件而且经过md5进行加密
      readStream.on('data', (d) => {
        console.log(333);
        console.log(d);
        md5.update(d);
      });
      readStream.on('end', () => {
        const eTag = md5.digest('hex');
        // 验证Etag 是否相同
        if (ifNoneMatch === eTag) {
          res.writeHead(304);
          res.end();
        } else {
          res.setHeader('Content-Type', mime.getType(filepath));
          // 第一次服务器返回的时候,会把文件的内容算出来一个标识,发给客户端
          fs.readFile(filepath, (err, content) => {
            // 客户端看到etag以后,也会把此标识保存在客户端,下次再访问服务器的时候,发给服务器
            res.setHeader('Etag', etag);
            // fs.createReadStream(filepath).pipe(res);
          });
        }
      });
    }
  });
  await next();
});
*/
// 咱们这边直接使用 现成的插件来简单的演示下。若是要比较的话,能够看上面的代码原理便可
import conditional from 'koa-conditional-get';
import etag from 'koa-etag';
app.use(conditional());
app.use(etag());

app.use(resource(path.join(__dirname, './static')));

app.listen(port, () => {
  console.log(`server is listen in ${host}:${port}`);
});

如上基本代码,当咱们第一次请求的时候(先清除浏览器缓存),能够看到以下图所示:

如上咱们能够看到返回值里面有Etag的值。

而后当咱们再次刷新浏览器代码的时候,浏览器将会带上 if-None-Match请求头,并赋值为上一次返回头的Etag的值。
而后和服务器端的Etag的值进行对比,若是相等的话,就会返回304 Not Modified。以下图所示:

咱们再来改下html的内容,咱们再来刷新下看看,能够看到页面内容发生改变了,所以Etag值是不同的。以下图所示

而后咱们继续刷新,就会返回304了,由于它会把最新的Etag的值赋值给 if-None-Match请求头,而后请求的时候,会把该最新值带过去,所以以下图所示能够看到。

如上就是协商缓存的基本原理了。下面咱们来看下强制缓存。

查看github源码

三:理解强制缓存

基本原理:浏览器在加载资源的时候,会先根据本地缓存资源的header中的信息(Expires 和 Cache-Control)来判断是否须要强制缓存。若是命中的话,则会直接使用缓存中的资源。不然的话,会继续向服务器发送请求。

Expires

Expires 是http1.0的规范,它的值是一个绝对时间的GMT格式的时间字符串。这个时间表明的该资源的失效时间,若是在该时间以前请求的话,则都是从缓存里面读取的。可是使用该规范时,可能会有一个缺点就是当服务器的时间和客户端的时间不同的状况下,会致使缓存失效。

Cache-Control

Cache-Control 是http1.1的规范,它是利用该字段max-age值进行判断的。该值是一个相对时间,好比 Cache-Control: max-age=3600, 表明该资源的有效期是3600秒。除了该字段外,咱们还有以下字段能够设置:

no-cache: 须要进行协商缓存,发送请求到服务器确认是否使用缓存。

no-store:禁止使用缓存,每一次都要从新请求数据。

public:能够被全部的用户缓存,包括终端用户和 CDN 等中间代理服务器。

private:只能被终端用户的浏览器缓存,不容许 CDN 等中继缓存服务器对其缓存。

Cache-Control 与 Expires 能够在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高。

下面咱们来看下使用 max-age 设置多少秒后过时来验证下。最基本的代码以下:

import path from 'path';
import Koa from 'koa';

//静态资源中间件
import resource from 'koa-static';
const app = new Koa();
const host = 'localhost';
const port = 7878;

app.use(async (ctx, next) => {
 // 设置响应头Cache-Control 设置资源有效期为300秒
  ctx.set({
    'Cache-Control': 'max-age=300'  
  });
  await next();
});

app.use(resource(path.join(__dirname, './static')));

app.listen(port, () => {
  console.log(`server is listen in ${host}:${port}`);
});

如上咱们设置了300秒后过时,也就是有效期为5分钟,当咱们第一次请求页面的时候,咱们能够查看下以下所示:

咱们能够看到响应头中有Cache-Control字段 max-age=300 这样的,而且状态码是200的状态。

下面咱们继续来刷新下页面,能够看到请求以下所示:

请求是200,可是数据是从内存里面读取,如上截图能够看到。

咱们如今再把该页面关掉,从新打开新的页面,打开控制台网络,再查看下能够看到以下所示:

由于内存是存在进程中的,当咱们关闭页面的时候,内存中的资源就被释放掉了,可是磁盘中的数据是永久的,如上咱们能够看到数据从硬盘中读取的。

如上设置的有效期为5分钟,5分钟事后咱们再来刷新下页面。以下所示:

如上能够看到 5分钟过时后,就不会从内存或磁盘中读取了,而是从新请求下服务器的资源。如上就是使用 max-age 来演示强制缓存的了。
查看github源码

相关文章
相关标签/搜索