“學過就忘”,一個知識點長時間不去讀取,那麼在你腦海中的基因片段會逐漸模糊,但是基因索引還是存在的。本篇文章通過實戰理論想結合的方式來幫助大家更深刻的記憶常用知識點。 如有不足之處,懇請斧正。
什麼是HTTP快取 ?
http
快取通俗來說: 當客戶端向伺服器請求資源時,不會直接向伺服器獲取資源(喂:伺服器,給我一張最新的自拍圖片)。而是先看下自己的儲存裡有沒有上次儲存的還在有效期內的資源。如果有的話,就省下了一筆運費(流量)。這裡舉一個簡單的例子:
- 客戶端需要
a.js
,於是傳送一個請求頭(1kb)
- 服務端響應後,返回
a.js(10kb)
+ 響應頭(1kb)
- 如此反覆每次就是
11kb
的流量傳送
但是我們我們需要的檔案a.js
的內容往往並沒有發生變化,卻仍然需要浪費流量,為了解決這個問題,於是人們提出了http
快取這個概念。
HTTP快取可以分為強快取和協商快取。
強快取
- Expires
- Cache-Control
- Pragma
協商快取
- Last-Modified
- If-Modified-Since
- ETag
- If-Not-Match
強快取與協商快取的區別:
使用者大人
:我現在需要a.js
,你們幫我拿回來
強快取
: 稍等,我找下我這裡有沒有關於a.js
的備份,找到了。(消耗0kb
流量)
協商快取
: 我這裡也有備份,不過我得問下服務端這個備份是不是最新款,傳送請求頭(1kb
流量)。服務端回覆(1kb
流量)響應頭則使用本地備份,若是返回a.js(10kb)
和響用頭(1kb
)則使用伺服器返回的最新資料。
強快取
Expires
這是
HTTP 1.0
的欄位,表示快取到期時間,是一個絕對的時間 (當前時間 + 快取時間)。在響應訊息頭中,請求資源前瀏覽器會用當前時間與其值對比,若是未過期則不需要再次請求。
新建cache.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:;base64,=">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>cache</title>
<script src="cache.js"></script>
</head>
<body>
<h1>cache</h1>
</body>
</html>
複製程式碼
新建cache.js
let http = require('http'),
fs = require('fs'),
path = require('path'),
url = require("url")
http.createServer((req,res)=>{
let pathname = __dirname + url.parse(req.url).pathname; // 獲取檔案路徑
let statusCode = 200 // 響應頭狀態碼
let extname = path.extname(pathname) // 獲取副檔名
let headType = 'text/html' // 內容型別
if( extname){
switch(extname){
case '.html':
headType = 'text/html'
break;
case '.js':
headType = 'text/javascript'
break;
}
}
fs.readFile(pathname, function (err, data) {
res.writeHead(statusCode, {
'Content-Type': headType,
'Expires':new Date(Date.now() + 10000).toUTCString() // 設定檔案過期時間為10秒後
});
res.end(data);
});
}).listen(3000)
console.log("Server running at http://127.0.0.1:3000/");
複製程式碼
啟動
node cache.js
複製程式碼
開啟瀏覽器訪問http://127.0.0.1:3000/cache.html
檢視network
- 首次訪問全部走網路請求資源
- 10s內再次重新整理從記憶體中載入(
from memory cache
) - 10s內關閉tab,重新開啟請求的cache.html,從磁碟載入(
from disk cache
) - 10s以後請求,快取已經失效,重複第1步
缺點:
Expires
過期控制不穩定。因為如果我們修改電腦的本地時間,會導致瀏覽器判斷快取失效。
Cache-control
這是
HTTP1.1
的欄位,這是一個相對時間'Cache-Control':'max-age=20'
意思是20秒內使用快取資源,不用請求伺服器
// 省略其他程式碼
fs.readFile(pathname, function (err, data) {
res.writeHead(statusCode, {
'Content-Type': headType,
'Cache-Control':'max-age=20'
});
複製程式碼
Cache-Control 除了max-age 相對過期時間以外,還有很多屬性
1. no-store
:所有內容都不快取
2. no-cache
:快取,但是瀏覽器使用快取前,都會請求伺服器判斷快取資源是否是最新,它是個比較高貴的存在,因為它只用不過期的快取。
3. public
客戶端和代理伺服器(CDN)都可快取
4. private
只有客戶端可以快取
更多屬性可參考 Cache-Control
這是因為瀏覽器會針對的使用者不同行為採用不同的快取策略,這樣會導致在不同的瀏覽器會產生不同的現象:Tips:也許大家在這裡有一些小疑問,為什麼對
cache.html
設定Expires
或Cache-Control
在谷歌瀏覽器裡不生效。這時候檢視request header
發現Cache-Control: max-age=0
,瀏覽器強制不用快取。
Chrome does something quite different: 'Cache-Control' is always set to 'max-age=0′, no matter if you press enter, f5 or ctrl+f5. Except if you start Chrome and enter the url and press enter
.
pragma
pragma是http/1.1之前版本的歷史遺留欄位,僅作為與http的向後相容而定義。這裡不做討論。感興趣的朋友可以點選瞭解 Pragma
node 熱更新
這裡如果大家覺得每次修改cache.js
檔案都需要重新執行node run cache.js
,這裡我們可以配置node 熱更新
npm init
npm i -D nodemon
複製程式碼
npm i -D
為npm install --save-dev
的縮寫
修改package.json
"scripts": {
"dev": "nodemon ./bin/www"
},
複製程式碼
下面我們只需要執行一次npm run dev
即可
對比快取(協商快取)
上面的強快取依舊存在著很大的缺陷。當設定的時間過期後,不管檔案內容有沒有變化,我們不得不再次向伺服器請求資源。這時候我們就需要用到協商快取了。
Last-Modified
響應值,由伺服器返回給客戶端關於請求資源的最近修改時間 (GMT標準格式)
If-Modified-Since
請求值 , 由客戶端傳送給服務端上次返回的資源修改時間
首次請求時服務端會攜帶Last-Modified
返回給客戶端,客戶端將其數值儲存起來,並重新命名為If-Modified-Since
再次請求時,客戶端會先傳送一個攜帶If-Modified-Since
的請求頭髮送到服務端,服務端會比較請求頭的If-Modified-Since
和伺服器請求資源上次的修改時間(Last-Modified)
.
- 如果資源已經被修改:那麼返回響應資源
a.js(10kb)
+ 響應頭(1kb)
,狀態碼:200 OK
- 如果沒有被修改:那麼只返回響應頭
(1kb)
,狀態碼:304 Not Modified
// 省略其他程式碼
let stat = fs.statSync(pathname);
fs.readFile(pathname, function (err, data) {
// 判斷請求頭的檔案修改時間是否等於服務端的檔案修改時間
if(req.headers['if-modified-since'] === stat.mtime.toUTCString()) { // mtime為檔案內容改變的時間戳
statusCode = 304;
}
res.writeHead(statusCode, {
'Content-Type': headType,
'Last-Modified':stat.mtime.toUTCString()
});
res.end(data);
});
複製程式碼
但使用Last-Modified同樣存在缺陷last-modified
是以秒為單位的,如果資源在1s內修改多次,由於1s內last-modified
並未改變,客戶端仍然會使用快取。- 如果在伺服器上請求的資源(
a.js
)被修改了,但其實際內容根本沒發生改變,會因為 Last-Modified 時間匹配不上而重新返回 a.js 給瀏覽器(舉例:伺服器動態生成檔案)
為了解決上述問題,使用新的欄位 ETag 和 If-None-Match
ETag
響應值,由伺服器返回給客戶端根據檔案內容,算出的一個唯一的值 (GMT標準格式)
If-Not-Match
請求值 , 由客戶端傳送給服務端上次返回的資源唯一值
請求流程與Last-Modified一致
上面我們所述
last_modified
一般由mtime
組成,而ETag
一般由last_modified
與content_length
組成
// 省略其他程式碼
fs.readFile(pathname, function (err, data) {
let Etag = `${data.length.toString(16)}${stat.mtime.toString(16)}`
if((req.headers['if-modified-since'] === stat.mtime.toUTCString()) || (req.headers['if-none-match'] === Etag)) {
statusCode = 304;
}
res.writeHead(statusCode, {
'Content-Type': headType,
Etag
});
res.end(data);
});
複製程式碼
快取優先順序
當多種快取同時存在時,瀏覽器改使用哪種快取方式呢?這裡就有了一個優先順序關係
- 強快取與協商快取同時存在時,如果強快取還在生效期則強制快取,否則使用協商快取。(強快取優先順序 > 對比快取優先順序)
- 強快取
expires
和cache-control
同時存在時,cache-control
會覆蓋expires
(cache-control
優先順序 >expires
優先順序。) - 對比快取
Etag
和Last-Modified
同時存在時,Etag
會覆蓋Last-Modified
效。(ETag
優先順序 >Last-Modified
)優先順序。
最佳實踐
上面只是讓大家更好的瞭解了http快取的大概知識點,那麼在實際開發中我們是如何如何利用快取實現更好的使用者體驗的呢?
相信webpack
大家已經並不陌生,如果有過實際配置經驗的同學一定記得我們在配置出口檔案output
或者打包圖片的filename
時往往會加上hash || chunkhash || contenthash
這些欄位
module.exports = {
output:{
path:path.resolve(__dirname,'../dist'),
filename:'js/[name].[contenthash:8].js',
chunkFilename:'js/[name].[chunkhash:8].js'
},
loader:{
rules:[
{
test:/\.(jep?g|png|gif)$/,
use:{
loader:'url-loader',
options:{
limit:10240,
fallback:{
loader:'file-loader',
options:{
name:'img/[name].[hash:8].[ext]'
}
}
}
}
}
]
}
}
複製程式碼
這樣打包出來的檔名稱往往如下
這樣如果每次檔案有變化,那麼檔名稱隨即變化。瀏覽器則會重新請求這些檔案資源。如果檔名稱與上次一致,那麼則會使用到http快取策略。