前言
大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心
背景
無論是開發中或者是面試中,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
的一樣,這裡就不重複說明了。。。
總結
參考資料
結語
我是林三心,一個熱心的前端菜鳥程式設計師。如果你上進,喜歡前端,想學習前端,那我們們可以交朋友,一起摸魚哈哈,摸魚群,加我請備註【思否】