理解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源碼

相關文章
相關標籤/搜索