node強緩存和協商緩存實戰示例

前言

瀏覽器緩存是性能優化非常重要的一個方案,合理地使用緩存可以提高用戶體驗,還能節省服務器的開銷。掌握好緩存的原理和並合理地使用無論對前端還是運維都是相當重要的。

什麼是瀏覽器緩存

瀏覽器緩存(http 緩存) 是指瀏覽器在本地磁盤對用戶最近請求過的文檔進行存儲,當訪問者再次訪問同一頁面時,瀏覽器就可以直接從本地磁盤加載文檔。

優點

減少瞭冗餘的數據傳輸,節省帶寬,減少服務器壓力

加快瞭客戶端加載速度,提升用戶體驗。

強緩存

強緩存不會向服務器發送請求,而是直接從緩存中讀取資源,強緩存可以通過設置兩種 HTTP Header 實現:Expires 和 Cache-Control,這兩個頭部分別是HTTP1.0和HTTP1.1的實現。

Expires

Expires是HTTP1.0提出的一個表示資源過期時間的header,它描述的是一個絕對時間,由服務器返回。

Expires 受限於本地時間,如果修改瞭本地時間,就會造成緩存失效。

Cache-Control

Cache-Control 出現於 HTTP/1.1,常見字段是max-age,單位是秒,很多web服務器都有默認配置,優先級高於Expires,表示的是相對時間。

例如Cache-Control:max-age=3600 代表資源的有效期是 3600 秒。取的是響應頭中的 Date,請求發送的時間,表示當前資源在 Date ~ Date +3600s 這段時間裡都是有效的。Cache-Control 還擁有多個值:

  • no-cache 不直接使用緩存,也就是跳過強緩存。
  • no-store 禁止瀏覽器緩存數據,每次請求資源都會向服務器要完整的資源。
  • public 可以被所有用戶緩存,包括終端用戶和 CDN 等中間件代理服務器。
  • private 隻允許終端用戶的瀏覽器緩存,不允許其他中間代理服務器緩存。

要註意的就是no-cache和no-store的區別,no-cache是跳過強緩存,還是會走協商緩存的步驟,而no-store是真正的完全不走緩存,所有資源都不會緩存在本地

協商緩存

當瀏覽器對某個資源的請求沒有命中強緩存,就會發一個請求到服務器,驗證協商緩存是否命中,如果協商緩存命中,請求響應返回的http狀態為304並且會顯示一個Not Modified的字符串。

協商緩存用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】這兩對Header來管理的。

註意!!協商緩存需要配合強緩存使用,使用協商緩存需要先設置Cache-Control:no-cache或者pragma:no-cache來告訴瀏覽器不走強緩存

Last-Modified、If-Modified-Since

這兩個Header是HTTP1.0版本提出來的,兩個字段配合使用。

Last-Modified 表示本地文件最後修改日期,瀏覽器會在請求頭帶上If-Modified-Since(上次返回的Last-Modified的值),服務器會將這個值與資源修改的時間匹配,如果時間不一致,服務器會返回新的資源,並且將 Last-Modified 值更新,作為響應頭返回給瀏覽器。如果時間一致,表示資源沒有更新,服務器返回 304 狀態碼,瀏覽器拿到響應狀態碼後從本地緩存中讀取資源。

但Last-Modified有幾個問題。

  • 文件雖然被修改瞭,但最終的內容沒有變化,這樣文件修改時間還是會被更新
  • 有的文件修改頻率在秒以內,這時候以秒粒度來記錄就不夠瞭
  • 有的服務器無法精確獲取文件的最後修改時間。

所以出現瞭ETAG。

ETag、If-None-Match

在HTTP1.1版本中,服務器通過 Etag 來設置響應頭緩存標識。Etag 的值由服務端生成。在第一次請求時,服務器會將資源和 Etag 一並返回給瀏覽器,瀏覽器將兩者緩存到本地緩存數據庫。在第二次請求時,瀏覽器會將 Etag 信息放到 If-None-Match 請求頭去訪問服務器,服務器收到請求後,會將服務器中的文件標識與瀏覽器發來的標識進行對比,如果不相同,服務器返回更新的資源和新的 Etag ,如果相同,服務器返回 304 狀態碼,瀏覽器讀取緩存。

image.png

流程總結

image.png

總結這幾個字段:

  • Cache-Control —— 請求服務器之前
  • Expires —— 請求服務器之前
  • If-None-Match (Etag) —— 請求服務器
  • If-Modified-Since (Last-Modified) —— 請求服務器

node實踐

本文用koa來做例子,因為koa是更輕量級的、更純凈的,本身並沒有捆綁任何中間件,相比express自帶瞭很多router、static等多種中間件函數,koa更適合本文來做示例。

koa啟動服務

秉著學習和更容易理解的宗旨,不使用koa-static和koa-router中間件,用koa簡易實現web服務器來驗證之前的結論。

創建項目

# 創建並進入一個目錄並新建index.js文件
mkdir koa-cache
cd koa-cache
touch index.js

# 初始化項目
git init
yarn init

# 將 koa 安裝為本地依賴
yarn add koa

koa代碼

/*app.js*/
const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
    ctx.body = 'hello koa'
})

app.listen(3000, () => {
  console.log('starting at port 3000')
})

啟動服務

node index.js

這樣一個koa服務就起來瞭,訪問localhost:3000可以就看到hello koa。

為瞭方便調試,修改代碼不用重新啟動,推薦使用nodemon或者pm2啟動服務。

原生koa實現簡易靜態資源服務

實現一個靜態資源服務器關鍵點就是根據前端請求的地址來判斷請求的資源類型,設置返回的Content-Type,讓瀏覽器知道返回的內容類型,瀏覽器才能決定以什麼形式,什麼編碼來讀取返回的內容。

定義資源類型列表

const mimes = {
  css: 'text/css',
  less: 'text/css',
  gif: 'image/gif',
  html: 'text/html',
  ico: 'image/x-icon',
  jpeg: 'image/jpeg',
  jpg: 'image/jpeg',
  js: 'text/javascript',
  json: 'application/json',
  pdf: 'application/pdf',
  png: 'image/png',
  svg: 'image/svg+xml',
  swf: 'application/x-shockwave-flash',
  tiff: 'image/tiff',
  txt: 'text/plain',
  wav: 'audio/x-wav',
  wma: 'audio/x-ms-wma',
  wmv: 'video/x-ms-wmv',
  xml: 'text/xml',
}

解析請求的資源類型

function parseMime(url) {
  // path.extname獲取路徑中文件的後綴名
  let extName = path.extname(url)
  extName = extName ? extName.slice(1) : 'unknown'
  return mimes[extName]
}

fs讀取文件

const parseStatic = (dir) => {
  return new Promise((resolve) => {
    resolve(fs.readFileSync(dir), 'binary')
  })
}

koa處理

app.use(async (ctx) => {
  const url = ctx.request.url
  if (url === '/') {
    // 訪問根路徑返回index.html
    ctx.set('Content-Type', 'text/html')
    ctx.body = await parseStatic('./index.html')
  } else {
    ctx.set('Content-Type', parseMime(url))
    ctx.body = await parseStatic(path.relative('/', url))
  }
})

這樣基本也就完成瞭一個簡單的靜態資源服務器。然後在根目錄下新建一個html文件和static目錄,並在static下放一些文件。這時候的目錄應該是這樣的:

|-- koa-cache
    |-- index.html
    |-- index.js
    |-- static
        |-- css
            |-- color.css
            |-- ...
        |-- image
            |-- soldier.png
            |-- ...
        ...
   ...

這時候就可以通過localhost:3000/static訪問具體的資源文件瞭。

index.html

<!DOCTYPE html>
<html lang="en">
 <head>
   <meta charset="UTF-8" />
   <meta http-equiv="X-UA-Compatible" content="IE=edge" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>test cache</title>
   <link rel="stylesheet" href="/static/css/index.css" rel="external nofollow"  />
 </head>
 <body>
   <div id="app">測試css文件</div>
   <img src="/static/image/soldier.png" alt="" />
 </body>
</html>

css/color.css

#app {
  color: blue;
}

這時候打開localhost:3000,就能看到如下效果:

image.png

到這裡基本的環境就都搭好瞭。接下來進入驗證階段。

強緩存驗證

在沒有任何配置之前,可以看下network:

image.png

這時候無論是首次還是第幾次,都會向服務器請求資源。

註意!!!在開始實驗之前要把network面板的Disable cache勾選去掉,這個選項表示禁用瀏覽器緩存,瀏覽器請求會帶上Cache-Control: no-cache和Pragma: no-cache頭部信息,這時候所有的請求都不會走緩存

image.png

設置Expire

修改index.js中的app.use代碼段。

app.use(async (ctx) => {
  const url = ctx.request.url
  if (url === '/') {
    // 訪問根路徑返回index.html
    ctx.set('Content-Type', 'text/html')
    ctx.body = await parseStatic('./index.html')
  } else {
    const filePath = path.resolve(__dirname, `.${url}`)
    ctx.set('Content-Type', parseMime(url))
    // 設置過期時間在30000毫秒,也就是30秒後
    ctx.set('Expires', new Date(Date.now() + 30000))
    ctx.body = await parseStatic(filePath)
  }
})

用ctx.set(‘Expires’, new Date(Date.now() + 30000)),設置過期時間為當期時間的30000毫秒,也就是30秒後(後面的設置頭部信息都是這裡修改)。

再訪問下localhost:3000,可以看到多瞭Expires這個Header。

image.png

後面在30秒之內訪問都可以看到network的Size,css文件顯示的是disk cache,而image資源顯示的是from memory cache。這時候瀏覽器是直接讀的瀏覽器緩存,並沒有請求服務器,可以嘗試把css和圖片文件改名稱或者刪除驗證下,頁面顯示正常,說明之前的結論是沒錯的。

image.png

Cache-Control

ctx.set(‘Cache-Control’, ‘max-age=300’)設置300秒有效期,驗證方式同上。

協商緩存驗證

Last-Modified,If-Modified-Since

HTTP1.0協商緩存關鍵點就是根據客戶端請求帶的ifModifiedSince字段的時間和請求的資源對應的修改時間來判斷資源是否有更新。

首先設置Cache-Control: no-cache, 使客戶端不走強緩存,再判斷客戶端請求是否有帶ifModifiedSince字段,沒有就設置Last-Modified字段,並返回資源文件。如果有就用fs.stat讀取資源文件的修改時間,並進行對比,如果時間一樣,則返回狀態碼304。

 ctx.set('Cache-Control', 'no-cache')
 const ifModifiedSince = ctx.request.header['if-modified-since']
 const fileStat = await getFileStat(filePath)
 if (ifModifiedSince === fileStat.mtime.toGMTString()) {
    ctx.status = 304
 } else {
    ctx.set('Last-Modified', fileStat.mtime.toGMTString())
    ctx.body = await parseStatic(filePath)
 }

etag、If-None-Match

etag的關鍵點在於計算資源文件的唯一性,這裡使用nodejs內置的crypto模塊來計算文件的hash值,並用十六進制的字符串表示。cypto的用法可以看nodejs的官網。

crpto不僅支持字符串的加密,還支持傳入buffer加密,作為nodejs的內置模塊,在這裡用來計算文件的唯一標識再合適不過。

    ctx.set('Cache-Control', 'no-cache')
    const fileBuffer = await parseStatic(filePath)
    const ifNoneMatch = ctx.request.headers['if-none-match']
    const hash = crypto.createHash('md5')
    hash.update(fileBuffer)
    const etag = `"${hash.digest('hex')}"`
    if (ifNoneMatch === etag) {
      ctx.status = 304
    } else {
      ctx.set('etag', etag)
      ctx.body = fileBuffer
    }

效果如下圖,第二次請求瀏覽器會帶上If-None-Match,服務器計算文件的hash值再次比較,相同則返回304,不同再返回新的文件。而如果修改瞭文件,文件的hash值也就變瞭,這時候兩個hash不匹配,服務器則返回新的文件並帶上新文件的hash值作為etag。

image.png

小結

通過以上代碼實踐瞭每個緩存字段的效果,代碼僅作為演示,生產的靜態資源服務器會更加復雜,例如etag不會每次都重新獲取文件來計算文件的hash值,這樣太費性能,一般都會有響應的緩存機制,比如對資源的 last-modified 和 etag 值建立索引緩存。

總結

通常web服務器都有默認的緩存配置,具體的實現可能也不大相同,像nginx、tomcat、express等web服務器都有相應的源碼,有興趣的可以去閱讀學習。

合理的使用強緩存和協商緩存具體需要看項目的使用場景和需求。像目前常見的單頁面應用,因為通常打包都是新生成html與相應的靜態資源依賴,所以可以對html文件配置協商緩存,而打包生成的依賴,例如js、css這些文件可以使用強緩存。或者隻對第三方庫使用強緩存,因為第三方庫通常版本更新較慢,可以鎖定版本。

node示例完整代碼 https://github.com/chen-junyi/code/blob/main/node/cache/koa2.js

以上就是node強緩存和協商緩存實戰示例的詳細內容,更多關於node強緩存協商緩存的資料請關註WalkonNet其它相關文章!

推薦閱讀: