輕鬆理解瀏覽器緩存(Koa緩存源碼解析)

1、緩存

緩存技術一直一來在WEB技術體系中扮演很是重要角色,是快速且有效地提高性能的手段。 css

緩存
如上圖,在網頁展現出來的過程當中,各個層面均可以進行緩存。

以前在學習緩存的過程當中,一直沒有實踐過,有些概念常常會忘記。 今天主要經過Node實踐的方式學習瀏覽器緩存,順便分析一下Koa處理緩存的源碼。前端

2、瀏覽器緩存

首先咱們看一下瀏覽器請求緩存過程。 java

緩存的處理過程

  • 發出請求後,會先在本地查找緩存。node

  • 查到有緩存,要判斷緩存是否新鮮(是否過時)。瀏覽器

  • 沒有過時,直接返回給客戶端。緩存

  • 若是緩存過時了,就要再次去服務器請求最新的資源,返回給客戶端,並從新進行緩存。安全

3、新鮮度檢測

可能不少同窗看其餘博客,提到都是「強緩存/協商緩存」等說法,這個我會放到後面講。 上圖中新鮮一詞比較少見,來自《HTTP權威指南》。服務器

由於HTTP會將資源緩存一段時間,在這個時間內,這個緩存就是「新鮮的」。 因此檢查緩存是否過時就被稱爲,新鮮度檢測框架

那麼接下來就經過Node來實戰一下,看看:koa

  1. 瀏覽器是如何進行緩存的?
  2. 如何進行新鮮度檢測?

4、Node實戰

上述提到緩存一段時間,那麼HTTP提供了通用首部字段(就是請求報文和響應報文都能用上的字段),來控制緩存時間。

1. Pragma/Expires介紹

1.0

Pragma 是HTTP/1.0標準中定義的一個header屬性,請求中包含Pragma的效果跟在頭信息中定義Cache-Control: no-cache相同,可是HTTP的響應頭沒有明肯定義這個屬性,因此它不能拿來徹底替代HTTP/1.1中定義的Cache-control頭。一般定義Pragma以向後兼容基於HTTP/1.0的客戶端。

Expires會返回一個絕對時間,若是請求時間在Expires指定的時間以前,就能命中緩存。可是由於客戶端能夠修改本地時間,會和和服務器時間不一致,容易出現差錯,不推薦使用。

Pragma/Expires

2. Cache-Control介紹

1.1

Cache-Control是如今常見的緩存方式,上述字段不少,初學者能夠只看max-age,避免混亂,也是最有意義的屬性。

max-age
Cache-Control描述的是一個相對時間,在進行緩存命中的時候,都是利用客戶端時間進行判斷,因此相比較 ExpiresCache-Control的緩存管理更有效,安全一些。

3. Cache-Control實戰

經過Koa框架,簡單搭建一個Node服務。並經過koa-static管理靜態資源。

代碼結構參考以下,maxage緩存設置10秒。

代碼
啓動服務,就能夠看到本身的頁面了~

node index.js // server is starting at port 8001
複製代碼

代碼目錄
在代碼截圖上,能夠看到給 koa-static傳了 maxage: 10 * 1000

koa-static源碼中引入了koa-send庫。截取部分koa-send源碼,只要傳入maxage,就會設置Cache-Controlmax-age。爲符合前端開發者習慣傳入爲毫秒,其實是用秒爲單位的。

經過 NetWork能夠觀察到已經成功設置 Cache-Control: max-age=10
緩存設置10s

訪問測試以下圖:

緩存驗證

  1. 在10s內再次請求,能夠看到js/css均來自緩存memory cache

  2. 10s後緩存過時,不走緩存,便再次從服務器獲取。

4. HTML爲什麼如此特殊?

4.1 現象

通過上面的實驗能夠看出,在Js/Css都走本地緩存的時候,HTML是依舊從服務端獲取的。

HTML爲什麼如此特殊
查看請求信息以後,發現請求頭中默認加上了 Cache-Control: max-age=0
HTML爲什麼如此特殊
通過測試,發現若是單獨請求 Js資源,也會出現此類現象。所以得出結論,這個是瀏覽器默認加的,應該是爲了 保證直接請求的資源最新
客戶端的緩存限制

4.2 緣由

針對request請求,若是有Cache-Control限制,那麼緩存系統就會先校驗Cache-Control。不符合規則就直接請求服務端,具體規則以下:

客戶端的新鮮度限制
客戶端的緩存限制
上述來自《HTTP權威指南》。 同理瀏覽器中 Network中的 disable-cache也是如此,發出請求時,表示不須要走緩存,必定要服務端最新的。
瀏覽器disable cache
no-cache

5. 服務端再驗證(新鮮度檢測)

上述不管是http1.0仍是1.1的方案,都是在本地緩存中存放一段時間。過時後就須要去服務端從新請求一遍。這個也被稱之爲強緩存

可是,緩存中過時並不意味服務端資源改變

所以請求發現本地緩存過時,能夠去服務端諮詢一下,這個資源還新鮮嗎?還能夠繼續使用嗎?常見的方法就是攜帶字段If-Modified-SinceIf-None-Match。若是驗證資源是新鮮的,沒有改變。那隻須要返回一個標識,也就是咱們常說的304,不須要返回數據,加速請求時間。

這個過程就是新鮮度檢測,那實現這個緩存的方式就是咱們常說的協商緩存

下面看下Node實戰協商緩存。

6. Last-Modified 和 If-Modified-Since

攜帶If-Modified-Since的前提是,緩存中存儲了Last-Modified字段。 每一個請求返回時,response中能夠攜帶字段Last-Modified,是服務端資源修改的最後日期。

下次發起請求時攜帶 If-Modified-Since就是緩存中的 Last-Modified,和服務端資源最後修改時間進行比較,就知道資源是否新鮮了。

6.1 代碼驗證

每一個請求返回時,response中能夠攜帶字段Last-Modified,是由於咱們使用的koa-static會默認給咱們的返回頭加上Last-Modified

發出的請求也會自動攜帶 If-Modified-Since
If-Modified-Since
可是驗證發現,10s後緩存過時,再次發出請求並無返回304,仍是200。
緣由是須要配置中間件 koa-conditional-get

6.2 koa-conditional-get

配置中間件`koa-conditional-get`
簡單看下 koa-conditional-get作了什麼,讓協商緩存生效。 能夠看出源碼很是簡單,判斷是否新鮮便可。

ctx.fresh如何計算,會在後面講。但很明顯是校驗了If-Modified-SinceLast-Modified

koa-conditional-get源碼

6.3 Last-Modified測試

  1. 在10s內請求
  2. 10s過時後請求
    304

測試結果,10s內Js/Css走強緩存。HTML因爲請求默認加max-age爲0,走協商緩存返回304,不須要返回數據,Size由484B降至163B。

304
10s後 Js/Css緩存到期,所有走協商緩存,因爲 Last-Modified一直沒有改變,均返回304,不須要返回數據, Size降至163B。 返回304後,會重置 max-age,10s內請求無需請求服務器,依然是強緩存。

  1. 修改Js內容

修改 Js內容測試結果, Css沒有修改依舊返回304。 Js修改致使 Last-Modified大於請求中的 If-Modified-Since,資源不夠新鮮,返回200並返回最新數據。

6.4 總結

Last-Modified工做流程以下:

通常來講,在沒有調整服務器時間和篡改客戶端緩存的狀況下,這兩個header配合起來管理協商緩存是很是可靠的,可是有時候也會服務器上資源其實有變化,可是最後修改時間卻沒有變化的狀況,而這種問題又很不容易被定位出來,而當這種狀況出現的時候,就會影響協商緩存的可靠性。因此就有了另一對header來管理協商緩存,這對header就是【ETagIf-None-Match

7. ETag 和 If-None-Match

7.1 ETag

這個header是服務器根據當前請求的資源生成的一個惟一標識,這個惟一標識是一個字符串,只要資源有變化這個串就不一樣,因此能很好的補充Last-Modified的問題。 避免干擾,能夠註釋Last-modified邏輯。

ETag的驗證也很是簡單,只須要再加入一箇中間件 koa-etag,重啓服務測試。

7.2 ETag實踐

發出請求,response已有Etag

下一次請求也會攜帶 If-None-Match爲緩存中的 Etag值:

修改Js資源測試,結果以下:

修改 Js資源測試後,致使 Etag改變。服務端再驗證資源不新鮮, Js資源從新獲取,返回200。
Css沒有修改, Etag沒變返回304。

Etag總體流程和Last-Modified保持一致。

7.3 koa-etag源碼解析

koaetag生成主要2個方法,具體的能夠直接去看源碼。

(1)根據文件的修改時間和文件大小生成

function stattag (stat) {
  var mtime = stat.mtime.getTime().toString(16)
  var size = stat.size.toString(16)

  return '"' + size + '-' + mtime + '"'
}
複製代碼

(2)使用crypto庫加密生成

function entitytag (entity) {
  if (entity.length === 0) {
    // fast-path empty
    return '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'
  }

  // compute hash of entity
  var hash = crypto
    .createHash('sha1')
    .update(entity, 'utf8')
    .digest('base64')
    .substring(0, 27)

  // compute length of entity
  var len = typeof entity === 'string'
    ? Buffer.byteLength(entity, 'utf8')
    : entity.length

  return '"' + len.toString(16) + '-' + hash + '"'
}

複製代碼

5、新鮮度檢測(Koa源碼解讀)

1. koa-conditional-get

在前面看到koa-conditional-get可讓協商緩存生效,緣由是對資源新鮮度作了304返回的處理。

koa-conditional-get源碼
那麼重點來看下 ctx.fresh是如何處理的?

2. koa

能夠看到Koa在request中的fresh方法以下:

狀態碼200-300之間以及304調用 fresh方法,判斷該請求的資源是否新鮮。

3. fresh方法源碼解讀

只保留核心代碼,能夠自行去看fresh的源碼。

var CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/

function fresh (reqHeaders, resHeaders) {
   // 1. 若是這2個字段,一個都沒有,不須要校驗
  var modifiedSince = reqHeaders['if-modified-since']
  var noneMatch = reqHeaders['if-none-match']
  if (!modifiedSince && !noneMatch) {
    console.log('not fresh')
    return false
  }

  // 2. 給端對端測試用的,由於瀏覽器的Cache-Control: no-cache請求
  // 是不會帶if條件的 不會走到這個邏輯
  var cacheControl = reqHeaders['cache-control']
  if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
    return false
  }

  // 3. 比較 etag和if-none-match
  if (noneMatch && noneMatch !== '*') {
    var etag = resHeaders['etag']

    if (!etag) {
      return false
    }
    // 部分代碼
    if (match === etag) {
        return true;
    }
  }
  
  // 4. 比較if-modified-since和last-modified
  if (modifiedSince) {
    var lastModified = resHeaders['last-modified']
    var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))
    if (modifiedStale) {
      return false
    }
  }
  
  return true
}
複製代碼

fresh的代碼判斷邏輯總結以下,知足3種條件之一,freshtrue

fresh代碼

6、總結

瀏覽器緩存總體流程以下:

瀏覽器緩存總體流程

  1. 發出請求後,會先在本地查找緩存。
  2. 沒有緩存去服務端請求最新的資源,返回給客戶端(200),並從新進行緩存。
  3. 查到有緩存,要判斷緩存本地是否過時(max-age等)。
  4. 沒有過時,直接返回給客戶端(200 from cache)。
  5. 若是緩存過時了,看是否有配置協商緩存(etag/last-modified),去服務端再驗證該資源是否更新,本地緩存是否能夠繼續使用。
  6. 若是發現資源可用,返回304,告知客戶端能夠繼續使用緩存,並根據max-age等更新緩存時間。不須要返回數據,加速請求時間。
  7. 若是服務端再驗證失敗,請求最新的資源,返回給客戶端(200),並從新進行緩存。

咱們常說的強緩存,其實就是直接在本地緩存獲取,也就是Cache-Control: max-age等配置,不須要和服務端溝通。

而協商緩存是在強緩存的基礎上,配置etag或last-modified等參數。本地緩存失效後,去服務端進行新鮮度檢測。能夠避免每次本地緩存過時後都返回最新的數據,形成請求緩慢。

7、參考資料

本文的源碼分析圍繞koa,不表明其餘服務框架。對這塊知識不瞭解建議實踐一下。寫錯的地方,接受批評指正~

相關文章
相關標籤/搜索