不廢話,程式碼實踐帶你掌握 強快取、協商快取!

Sunshine_Lin發表於2022-02-24

前言

大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心

背景

無論是開發中或者是面試中,HTTP快取都是非常重要的,這體現在了兩個方面:

  • 開發中:合理利用 HTTP快取 可以提高前端頁面的效能
  • 面試中 HTTP快取 是面試中的高頻問點

所以本篇文章,我不講廢話,我就通過 Nodejs 的簡單實踐,給大家講最通俗易懂的HTTP快取,大家通過這篇文章一定能瞭解掌握它!!!

前置準備

準備

  • 建立資料夾 cache-study ,並準備環境

    npm init
  • 安裝 Koa、nodemon

    npm i koa -D
    npm i nodemon -g
  • 建立 index.js、index.html、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>Document</title>
    <link rel="stylesheet" href="./static/css/index.css">
    </head>
    <body>
    <div class="box">
    
    </div>
    </body>
    </html>
  • static/css/index.css

    .box {
    width: 500px;
    height: 300px;
    background-image: url('../image/guang.jpg');
    background-size: 100% 100%;
    color: #000;
    
    }
  • static/image/guang.jpg

  • index.js

    const Koa = require('koa')
    const fs = require('fs')
    const path = require('path')
    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]
    }
    
    // 將檔案轉成傳輸所需格式
    const parseStatic = (dir) => {
    return new Promise((resolve) => {
      resolve(fs.readFileSync(dir), 'binary')
    })
    }
    
    const app = new 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 {
      const filePath = path.resolve(__dirname, `.${url}`)
      // 設定型別
      ctx.set('Content-Type', parseMime(url))
      // 設定傳輸
      ctx.body = await parseStatic(filePath)
    }
    })
    
    app.listen(9898, () => {
    console.log('start at port 9898')
    })

    啟動頁面

    現在你可以在終端中輸入 nodemon index ,看到下方的顯示,則代表成功啟動了服務

此時你可以在瀏覽器連結裡輸入 http://localhost:9898/ ,開啟看到如下頁面,則代表頁面訪問成功!!!

HTTP快取種類

HTTP快取 常見的有兩類:

  • 強快取 :可以由這兩個欄位其中一個決定

    • expires
    • cache-control(優先順序更高)
  • 協商快取 :可以由這兩對欄位中的一對決定

    • Last-Modified,If-Modified-Since
    • Etag,If-None-Match(優先順序更高)

強快取

接下來我們就開始講強快取

expires

我們只需設定響應頭裡 expires 的時間為 當前時間 + 30s 就行了

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))
    // 設定 Expires 響應頭
    const time = new Date(Date.now() + 30000).toUTCString()
    ctx.set('Expires', time)
    // 設定傳輸
    ctx.body = await parseStatic(filePath)
  }
})

然後我們在前端頁面重新整理,我們可以看到請求的資源的響應頭裡多了一個 expires 的欄位

並且,在30s內,我們重新整理之後,看到請求都是走 memory ,這意味著,通過 expires 設定強快取的時效是30s,這30s之內,資源都會走本地快取,而不會重新請求

注意點:有時候你Nodejs程式碼更新了時效時間,但是發現前端頁面還是在走上一次程式碼的時效,這個時候,你可以把這個 Disabled cache 打鉤,然後重新整理一下,再取消打鉤

cache-control

其實 cache-control expires 效果差不多,只不過這兩個欄位設定的值不一樣而已,前者設定的是 秒數 ,後者設定的是 毫秒數

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))
    // 設定 Cache-Control 響應頭
    ctx.set('Cache-Control', 'max-age=30')
    // 設定傳輸
    ctx.body = await parseStatic(filePath)
  }
})

前端頁面響應頭多了 cache-control 這個欄位,且30s內都走本地快取,不會去請求服務端

協商快取

強快取 不同的是, 強快取 是在時效時間內,不走服務端,只走本地快取;而 協商快取 是要走服務端的,如果請求某個資源,去請求服務端時,發現 命中快取 則返回 304 ,否則則返回所請求的資源,那怎麼才算 命中快取 呢?接下來講講

Last-Modified,If-Modified-Since

簡單來說就是:

  • 第一次請求資源時,服務端會把所請求的資源的 最後一次修改時間 當成響應頭中 Last-Modified 的值發到瀏覽器並在瀏覽器存起來
  • 第二次請求資源時,瀏覽器會把剛剛儲存的時間當成請求頭中 If-Modified-Since 的值,傳到服務端,服務端拿到這個時間跟所請求的資源的最後修改時間進行比對
  • 比對結果如果兩個時間相同,則說明此資源沒修改過,那就是 命中快取 ,那就返回 304 ,如果不相同,則說明此資源修改過了,則 沒命中快取 ,則返回修改過後的新資源
// 獲取檔案資訊
const getFileStat = (path) => {
  return new Promise((resolve) => {
    fs.stat(path, (_, stat) => {
      resolve(stat)
    })
  })
}

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}`)
    const ifModifiedSince = ctx.request.header['if-modified-since']
    const fileStat = await getFileStat(filePath)
    console.log(new Date(fileStat.mtime).getTime())
    ctx.set('Cache-Control', 'no-cache')
    ctx.set('Content-Type', parseMime(url))
    // 比對時間,mtime為檔案最後修改時間
    if (ifModifiedSince === fileStat.mtime.toGMTString()) {
      ctx.status = 304
    } else {
      ctx.set('Last-Modified', fileStat.mtime.toGMTString())
      ctx.body = await parseStatic(filePath)
    }
  }
})

第一次請求時,響應頭中:

第二次請求時,請求頭中:

由於資源並沒修改,則命中快取,返回304:

此時我們修改一下 index.css

.box {
  width: 500px;
  height: 300px;
  background-image: url('../image/guang.jpg');
  background-size: 100% 100%;
  /* 修改這裡 */
  color: #333;
}

然後我們重新整理一下頁面, index.css 變了,所以會 沒命中快取 ,返回200和新資源,而 guang.jpg 並沒有修改,則 命中快取 返回304:

Etag,If-None-Match

其實 Etag,If-None-Match Last-Modified,If-Modified-Since 大體一樣,區別在於:

  • 後者是對比資源最後一次修改時間,來確定資源是否修改了
  • 前者是對比資源內容,來確定資源是否修改

那我們要怎麼比對資源內容呢?我們只需要讀取資源內容,轉成hash值,前後進行比對就行了!!

const crypto = require('crypto')

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}`)
    const fileBuffer = await parseStatic(filePath)
    const ifNoneMatch = ctx.request.header['if-none-match']
    // 生產內容hash值
    const hash = crypto.createHash('md5')
    hash.update(fileBuffer)
    const etag = `"${hash.digest('hex')}"`
    ctx.set('Cache-Control', 'no-cache')
    ctx.set('Content-Type', parseMime(url))
    // 對比hash值
    if (ifNoneMatch === etag) {
      ctx.status = 304
    } else {
      ctx.set('etag', etag)
      ctx.body = fileBuffer
    }
  }
})

驗證方式跟剛剛 Last-Modified,If-Modified-Since 的一樣,這裡就不重複說明了。。。

總結

參考資料

結語

我是林三心,一個熱心的前端菜鳥程式設計師。如果你上進,喜歡前端,想學習前端,那我們們可以交朋友,一起摸魚哈哈,摸魚群,加我請備註【思否】

image.png

相關文章