用node搞web服務和直接用tomcat、Apache做伺服器不太一樣, 很多工作都需要自己做。緩存策略也要自己選擇,雖然有像koa-static,express.static這些東西可以用來管理靜態資源,但是為了開發或配置時更加得心應手,知其所以然,有瞭解http緩存的必要。另外,http緩存作... ...
用node搞web服務和直接用tomcat、Apache做伺服器不太一樣, 很多工作都需要自己做。緩存策略也要自己選擇,雖然有像koa-static,express.static這些東西可以用來管理靜態資源,但是為了開發或配置時更加得心應手,知其所以然,有瞭解http緩存的必要。另外,http緩存作為一個前端優化的一個要點,也應該有所瞭解。
什麼是http緩存
RFC 7234 (https://tools.ietf.org/pdf/rfc7234.pdf)指出HTTP緩存是響應消息的本地存儲,並且是控制其中消息的存儲、檢索和刪除的子系統。
通俗講: http協議規定了一些指令, 實現http協議的伺服器和瀏覽器根據這些指令決定要不要以及如何把響應存儲起來以備後續使用.
http緩存的意義
- 提高響應速度
- 減少帶寬占用, 省流量
- 減小伺服器壓力
不指定任何與緩存有關的指令
這種情況下瀏覽器不做緩存, 每次都會想伺服器請求. 但是比較奇怪的是在nginx的實現中, 這種情況下還是被代理伺服器做了緩存.也就是說, 當多次請求同一個資源時, 代理伺服器只向源伺服器請求一次.
演示第1個例子nothing_1
強制緩存
所謂強制緩存就是給出資源的到期時間
expires
或者有效時間max-age
, 在這個時間之內該資源應該被緩存.如何讓一個資源被強緩存
1.expires
這個欄位定義了一個資源到期的時間. 看一個實際的例子:
可以看到這個expires
是個GMT
時間, 它的工作機制是, 首次請求時, 伺服器在響應中加上expires
標識資源的到期時間, 瀏覽器緩存這個資源, 再次請求時, 瀏覽器將上一次請求到這個資源的過期時間與自己的系統時間對比, 若系統時間小於過期時間, 則證明資源沒有過期, 直接用上次緩存的資源, 不必請求; 否則重新請求, 伺服器在響應中給出新的過期時間.
演示第9個例子expires_9
const d = new Date(Date.now() + 5000);
res.writeHead(200, {
'Content-Type': 'image/png',
'expires': d.toGMTString()
});
res.end(img);
2.Cache-Control:[public | private,] max-age=\({n}, s-maxage=\){m}
expires
存在的問題是他依賴於客戶端的系統時間, 客戶端系統時間錯誤可能會引起判斷錯誤. HTTP1.1增加了Cache-Control
解決此問題, 這個指令值比較豐富, 常見的如下:
- public/private: 標識資源能不能被代理伺服器緩存,
public
標識資源既能被代理伺服器緩存也能被瀏覽器緩存,private
標識資源只能被瀏覽器緩存, 不能被代理伺服器緩存. - max-age: 用於指定在客戶端緩存的有效時間, 單位s, 超過n秒需要重新請求, 不超過則可以使用緩存
- s-maxage: 這個是針對代理伺服器的, 表示資源在代理伺服器緩存時間沒有超過這個時間不必向源伺服器請求, 否則需要.
- no-cache: 有這個指令表示不走瀏覽器緩存了, 協商緩存還可以走
- no-store: 強制無緩存, 協商緩存也不走了, 測試發下即使響應中有
Last-Modified
, 瀏覽器請求時頁不會帶If-Modified-Since
一個實例
演示第2,3,4,5,7
協商緩存
- 所謂協商緩存就是客戶端想用緩存資源時先向伺服器詢問, 如果伺服器如果認為這個資源沒有過期, 可以繼續用則給出304響應, 客戶端繼續使用原來的資源; 否則給出200, 併在響應body加上資源, 客戶端使新的資源.
1.Last-Modified與If-Modified-Since
這個機制是, 伺服器在響應頭中加上Last-Modified
, 一般是一個資源的最後修改時間, 瀏覽器首次請求時獲得這個時間, 下一次請求時將這個時間放在請求頭的If-Modified-Since
, 伺服器收到這個If-Modified-Since
時間n
後查詢資源的最後修改時間m
與之對比, 若m>n
, 給出200響應, 更新Last-Modified
為新的值, body中為這個資源, 瀏覽器收到後使用新的資源; 否則給出304響應, body無數據, 瀏覽器使用上一次緩存的資源.
2.Etag與If-None-Match
Last-Modified
模式存兩個問題, 一是它是秒級別的比對, 所以當資源的變化小於一秒時瀏覽器可能使用錯誤的資源; 二是資源的最新修改時間變了可能內容並沒有變, 但是還是會給出完整響應, 造成浪費. 基於此在HTTP1.1引入了Etag模式.
這個與上面的Last-Modified
機制基本相同, 不過不再是比對最後修改時間而是比對資源的標識, 這個Etag一般是基於資源內容生成的標識. 由於Etag是基於內容生成的, 所以當且僅當內容變化才會給出完整響應, 無浪費和錯誤的問題.
演示第8, 10
如何選擇緩存策略
附錄
1.演示代碼
const http = require('http');
const fs = require('fs');
let etag = 0;
let tpl = fs.readFileSync('./index.html');
let img = fs.readFileSync('./test.png');
http.createServer((req, res) => {
etag++; // 我是個假的eTag
console.log('--->', req.url);
switch (req.url) {
// 模板
case '/index':
res.writeHead(200, {
'Content-Type': 'text/html',
'Cache-Control': 'no-store'
});
res.end(tpl);
break;
// 1. 不給任何與緩存相關的頭, 任何情況下, 既不會被瀏覽器緩存, 也不會被代理服務緩存
case '/img/nothing_1':
res.writeHead(200, {
'Content-Type': 'image/png'
});
res.end(img);
break;
// 2. 設置了no-cache表明每次要使用緩存資源前需要向伺服器確認
case '/img/cache-control=no-cache_2':
res.writeHead(200, {
'Content-Type': 'image/png',
'cache-control': 'no-cache'
});
res.end(img);
break;
// 3. 設置max-age表示在瀏覽器最多緩存的時間
case '/img/cache-control=max-age_3':
res.writeHead(200, {
'Content-Type': 'image/png',
'cache-control': 'max-age=10'
});
res.end(img);
break;
// 4. 設置了max-age s-maxage public: public 是說這個資源可以被伺服器緩存, 也可以被瀏覽器緩存,
// max-age意思是瀏覽器的最長緩存時間為n秒, s-maxage表明代理伺服器的最長緩存時間為那麼多秒
case '/img/cache-control=max-age_s-maxage_public_4':
res.writeHead(200, {
'Content-Type': 'image/png',
'cache-control': 'public, max-age=10, s-maxage=40'
});
res.end(img);
break;
// 設置了max-age s-maxage private: private 是說這個資源只能被瀏覽器緩存, 不能被代理伺服器緩存
// max-age說明瞭在瀏覽器最長緩存時間, 這裡的s-maxage實際是無效的, 因為不能被代理服務緩存
case '/img/cache-control=max-age_s-maxage_private_5':
res.writeHead(200, {
'Content-Type': 'image/png',
'cache-control': 'private, max-age=10, s-maxage=40'
});
res.end(img);
break;
// 7. 可以被代理伺服器緩存, 確不能被瀏覽器緩存
case '/img/cache-control=private_max-age_7':
res.writeHead(200, {
'Content-Type': 'image/png',
'cache-control': 'public, s-maxage=40'
});
res.end(img);
break;
// 8. 協商緩存
case '/img/talk_8':
let stats = fs.statSync('./test.png');
let mtimeMs = stats.mtimeMs;
let If_Modified_Since = req.headers['if-modified-since'];
let oldTime = 0;
if(If_Modified_Since) {
const If_Modified_Since_Date = new Date(If_Modified_Since);
oldTime = If_Modified_Since_Date.getTime();
}
mtimeMs = Math.floor(mtimeMs / 1000) * 1000; // 這種方式的精度是秒, 所以毫秒的部分忽略掉
console.log('mtimeMs', mtimeMs);
console.log('oldTime', oldTime);
if(oldTime < mtimeMs) {
res.writeHead(200, {
'Cache-Control': 'no-cache',
// 測試發現, 必須要有max-age=0 或者no-cache,或者expires為當前, 才會協商, 否則沒有協商的過程
'Last-Modified': new Date(mtimeMs).toGMTString()
});
res.end(fs.readFileSync('./test.png'));
}else {
res.writeHead(304);
res.end();
}
// 9. 設置了expires, 表示資源到期時間
case '/img/expires_9':
const d = new Date(Date.now() + 5000);
res.writeHead(200, {
'Content-Type': 'image/png',
'expires': d.toGMTString()
});
res.end(img);
break;
// 10. 設置了expires, 表示資源到期時間
case '/img/etag_10':
const If_None_Match = req.headers['if-none-match'];
console.log('If_None_Match,',If_None_Match);
if(If_None_Match != etag) {
res.writeHead(200, {
'Content-Type': 'image/png',
'Etag': String(etag)
});
res.end(img);
}else {
res.statusCode = 304;
res.end();
}
break;
// 11. no-store 能協商緩存嗎? 不能, 請求不會帶if-modified-since
case '/img/no-store_11':
const stats2 = fs.statSync('./test.png');
let mtimeMs2 = stats2.mtimeMs;
let If_Modified_Since2 = req.headers['if-modified-since'];
let oldTime2 = 0;
if(If_Modified_Since2) {
const If_Modified_Since_Date = new Date(If_Modified_Since2);
oldTime2 = If_Modified_Since_Date.getTime();
}
mtimeMs2 = Math.floor(mtimeMs2 / 1000) * 1000; // 這種方式的精度是秒, 所以毫秒的部分忽略掉
console.log('mtimeMs', mtimeMs2);
console.log('oldTime', oldTime2);
if(oldTime2 < mtimeMs2) {
res.writeHead(200, {
'Cache-Control': 'no-store',
// 測試發現, 必須要有max-age=0 或者no-cache,或者expires為當前, 才會協商, 否則沒有協商的過程
'Last-Modified': new Date(mtimeMs2).toGMTString()
});
res.end(fs.readFileSync('./test.png'));
}else {
res.writeHead(304);
res.end();
}
default:
res.statusCode = 404;
res.statusMessage = 'Not found',
res.end();
}
}).listen(1234);
2.測試用代理伺服器nginx配置
不要問我這是個啥, 我是copy的
worker_processes 8;
events {
worker_connections 65535;
}
http {
include mime.types;
default_type application/octet-stream;
charset utf-8;
log_format main '$http_x_forwarded_for $remote_addr $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_cookie" $host $request_time';
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
proxy_connect_timeout 500;
#跟後端伺服器連接的超時時間_發起握手等候響應超時時間
proxy_read_timeout 600;
#連接成功後_等候後端伺服器響應的時間_其實已經進入後端的排隊之中等候處理
proxy_send_timeout 500;
#後端伺服器數據回傳時間_就是在規定時間內後端伺服器必須傳完所有數據
proxy_buffer_size 128k;
#代理請求緩存區_這個緩存區間會保存用戶的頭信息以供Nginx進行規則處理_一般只要能保存下頭信息即可
proxy_buffers 4 128k;
#同上 告訴Nginx保存單個用的幾個Buffer最大用多大空間
proxy_busy_buffers_size 256k;
#如果系統很忙的時候可以申請更大的proxy_buffers 官方推薦*2
proxy_temp_file_write_size 128k;
#設置web緩存區名為cache_one,記憶體緩存空間大小為12000M,自動清除超過15天沒有被訪問過的緩存數據,硬碟緩存空間大小200g
#要想開啟nginx的緩存功能,需要添加此處的兩行內容!
#設置Web緩存區名稱為cache_one,記憶體緩存空間大小為500M,緩存的數據超過1天沒有被訪問就自動清除;訪問的緩存數據,硬碟緩存空間大小為30G
proxy_cache_path /usr/local/nginx/proxy_cache_path levels=1:2 keys_zone=cache_one:500m inactive=1d max_size=30g;
#創建緩存的時候可能生成一些臨時文件存放的位置
proxy_temp_path /usr/local/nginx/proxy_temp_path;
fastcgi_connect_timeout 3000;
fastcgi_send_timeout 3000;
fastcgi_read_timeout 3000;
fastcgi_buffer_size 256k;
fastcgi_buffers 8 256k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
fastcgi_intercept_errors on;
client_header_timeout 600s;
client_body_timeout 600s;
client_max_body_size 100m;
client_body_buffer_size 256k;
gzip off;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 9;
gzip_types text/plain application/x-javascript text/css application/xml text/javascript;
gzip_vary on;
include vhosts/*.conf;
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://127.0.0.1:1234;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_cache cache_one;
#此處的cache_one必須於上一步配置的緩存區功能變數名稱稱相同
proxy_cache_valid 200 304 12h;
proxy_cache_valid 301 302 1d;
proxy_cache_valid any 1h;
#不同的請求設置不同的緩存時效
proxy_cache_key $uri$is_args$args;
#生產緩存文件的key,通過4個string變數結合生成
expires off;
#加了這個的話會自己修改cache-control, 寫成off則不會
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
參考文獻
https://juejin.im/book/5b936540f265da0a9624b04b/section/5b9ba651f265da0ac726e5de
這是一個付費的冊子,可能沒法訪問