理解http瀏覽器的協商緩存和強制緩存


閱讀目錄

一:瀏覽器緩存的作用是什么?

1. 緩存可以減少冗余的數據傳輸。節省了網絡帶寬,從而更快的加載頁面。
2. 緩存降低了服務器的要求,從而服務器更快的響應。

那么我們使用緩存,緩存的資源文件到什么地方去了呢?

那么首先來看下 memory cache 和 disk cache 緩存

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM