一. 背景
最近看了不少關於客戶端快取機制的文章,大概弄明白了整個原理,但是對於中間一些細節一直有點迷糊,下面的內容是自己做的驗證
二. 準備工作
http快取的基本情況可以看看下這些文章
常見的影響快取的配置有以下幾個:
- 原伺服器的
Expires
- 原伺服器的
Cache-Control
- 代理伺服器(nginx)
Expires
- 代理伺服器(nginx)
proxy_cache_valid
- 代理伺服器(nginx)
proxy_cache_path
中的inactive
當前這篇會先驗證無代理伺服器的情況,有代理的情況放在之後驗證
配置說明
原伺服器的Cache-Control
:
名稱 | 說明 |
---|---|
private | 客戶端可以快取,代理伺服器不可以快取 |
public | 客戶端和代理伺服器都可以快取 |
max-age=t | 快取內容將在t秒後失效 |
no-cache | 使用協商緩 |
no-store | 不使用快取 |
原伺服器的Expires
:
這是http 1.0的屬性,現在應該用的少了;該屬性設定的是一個過期時間,過期時間內命中強快取;過期時間外,協商快取
三. 驗證
以下是我準備驗證的問題:
- 原伺服器的
Cache-Control
不同屬性的實際情況 - 原伺服器的
Expires
的實際情況 - 不使用代理伺服器的情況下,原伺服器的
Expires
和Cache-Control
同時存在,那個優先順序高
[1-24 更新] 增加一個驗證:
- 如果上述頭都未設定,快取情況是怎樣的?
1. 首先是無代理伺服器的情況
驗證方式是使用node的express啟動一個服務
const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express();
const port = 3030;
app.use(express.static(path.resolve(__dirname, './')));
app.get('/index', (req, res) => {
const html = fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8');
res.send(html)
})
// cache驗證
app.all('*', (req, res, next) => {
// res.header('Cache-Control', 'private');
// res.header('Cache-Control', 'public');
res.header('Cache-Control', 'max-age=20');
// res.header('Cache-Control', 'no-cache');
// res.header('Cache-Control', 'no-store');
next();
})
const questions = [
{
id: '000',
name: 'Rose'
},
{
id: '111',
name: 'Jack'
}
]
app.get('/api/data', (req, res) => {
res.status(200);
res.json(questions);
})
// 監聽埠
app.listen(port, () => {
console.log(`success listen at ${port}`);
})
複製程式碼
1.1 Cache-Control的各屬性驗證
ps:因為不存在代理伺服器,所以public和private的區別現在是看不出來的,我們之後和有代理的情況一起驗證
// 單獨驗證Cache-Control
app.all('*', (req, res, next) => {
res.header('Cache-Control', 'max-age=30');
// res.header('Cache-Control', 'no-cache');
// res.header('Cache-Control', 'no-store');
next();
})
複製程式碼
以下驗證可以得出結論:
- max-age未過期 -> 命中強快取
- max-age過期 -> 資源未修改 -> 命中協商快取
- max-age過期 -> 資源已修改 -> 伺服器獲取資源
- no-cache -> 資源未修改 -> 命中協商快取
- no-cache -> 資源已修改 -> 伺服器獲取資源
- no-store -> 伺服器獲取資源
Cache-Control | 請求延遲時間 | 資源是否改變 | If-None-Match(request) | ETag(respanse) | Status Code | 結論 |
---|---|---|---|---|---|---|
max-age=30 | 10s | 是 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 200 OK (from disk cache) | 強快取未過期,命中強快取 |
max-age=30 | 60s | 是 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"3a-LT60UfEg/Jmv4cmkNOAvZSUh6Qo" | 200 OK | 強快取過期,資源被修改,重新從伺服器獲取資源 |
max-age=30 | 10s | 否 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 200 OK (from disk cache) | 強快取未過期,命中強快取 |
max-age=30 | 60s | 否 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 304 Not Modified | 強快取過期,資源未修改,命中協商快取 |
no-cache | 30s | 是 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"3a-LT60UfEg/Jmv4cmkNOAvZSUh6Qo" | 200 OK | 資源被修改,未命中協商快取,從伺服器獲取資源 |
no-cache | 30s | 否 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 304 Not Modified | 資源未修改,命中協商快取 |
no-store | 30s | 是 | 無 | W/"3a-LT60UfEg/Jmv4cmkNOAvZSUh6Qo" | 200 OK | 不使用快取,直接從服務端獲取資源 |
no-store | 30s | 否 | 無 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 200 OK | 不使用快取,直接從服務端獲取資源 |
1.2 Expires驗證
ps1:如果這個Expires
欄位後端沒有處理過的話,返回的應該是GMT的標準時間Wed, 23 Jan 2019 09:52:55 GMT
,也就是格林威治的標準時間;而我們一般使用的是本地時間Wed Jan 23 2019 18:06:15 GMT+0800 (中國標準時間)
,所以要做一下適當的處理
ps2:我是在chrome上用localhost上測試的,但不知道為什麼設定Expires
後,不管過沒過期,不管有沒有同時設定Cache-Control
,Status Code
狀態一直是304的,聽說好像是因為用了localhost的關係,這個與線上並不一定完全一致;這裡關於Expires
的測試我是加了nginx代理了之後的結果,不過代理的快取沒有設定
ps3:Expires
的結果就僅供參考吧,以上
const moment = require('moment');
// cache驗證
app.all('*', (req, res, next) => {
res.header('Expires', getGLNZ());
next();
})
// 轉換格林威治時間
function getGLNZ() {
return moment().utc().add(30, 's').format('ddd, DD MMM YYYY HH:mm:ss') + ' GMT';
}
複製程式碼
以下驗證可以得出結論:
- Expires未過期 -> 命中強快取
- Expires過期 -> 資源未修改 -> 命中協商快取
- Expires過期 -> 資源已修改 -> 伺服器獲取資源
Expires | 請求延遲時間 | 資源是否改變 | If-None-Match(request) | ETag(respanse) | Status Code | 結論 |
---|---|---|---|---|---|---|
當前時間+30s | 10s | 否 | 無 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 200 OK (from disk cache) | 強快取未過期,命中強快取 |
當前時間+30s | 60s | 否 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 304 Not Modified | 強快取過期,資源未修改,命中協商快取 |
當前時間+30s | 10s | 是 | 無 | W/"3a-LT60UfEg/Jmv4cmkNOAvZSUh6Qo" | 200 OK (from disk cache) | 強快取未過期,命中強快取 |
當前時間+30s | 60s | 是 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"3a-LT60UfEg/Jmv4cmkNOAvZSUh6Qo" | 200 OK | 強快取過期,資源被修改,重新從伺服器獲取資源 |
1.3 Expires與Cache-Control優先順序
結論:Cache-Control
優先順序比Expires
優先順序高
const moment = require('moment');
// cache驗證
app.all('*', (req, res, next) => {
res.header('Cache-Control', 'max-age=60');
// res.header('Cache-Control', 'no-cache');
// res.header('Cache-Control', 'no-store');
res.header('Expires', getGLNZ());
next();
})
function getGLNZ(){
return moment().utc().add(30, 's').format('ddd, DD MMM YYYY HH:mm:ss') + ' GMT';
}
複製程式碼
以下驗證可以看出,在Cache-Control
與Expires
同時設定的情況下,Expires
是失效的
Expires | Cache-Control | 請求延遲時間 | 資源是否改變 | If-None-Match(request) | ETag(respanse) | Status Code | 結論 |
---|---|---|---|---|---|---|---|
當前時間+30s | max-age=60 | 10s | 否 | 無 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 200 OK (from disk cache) | Expires與Cache-Control都未過期,命中強快取 |
當前時間+30s | max-age=60 | 40s | 否 | 無 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 200 OK (from disk cache) | Expires過期,Cache-Control未過期,命中強快取 |
當前時間+30s | max-age=60 | 100s | 否 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 304 Not Modified | Expires與Cache-Control都過期,資源未修改,命中協商快取 |
當前時間+30s | no-cache | 10s | 否 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 304 Not Modified | Expires未過期,但是命中協商快取 |
當前時間+30s | no-cache | 60s | 否 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 304 Not Modified | Expires過期,命中協商快取 |
當前時間+30s | no-store | 10s | 否 | 無 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 200 OK | Expires未過期,直接獲取服務端資源 |
當前時間+30s | no-store | 60s | 否 | 無 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 200 OK | Expires過期,直接獲取服務端資源 |
1.4 Expires與Cache-Control都未設定
以下驗證結論:
If-None-Match
與ETag
相同 -> 協商快取If-None-Match
與ETag
不相同 -> 伺服器獲取資源
資源是否改變 | If-None-Match(request) | ETag(respanse) | Status Code | 結論 |
---|---|---|---|---|
是 | W/"3a-LT60UfEg/Jmv4cmkNOAvZSUh6Qo" | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 200 OK | 伺服器獲取資源 |
否 | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | W/"37-W4vHvR7+YshQyC9DoyT14egVUqo" | 304 Not Modified | 命中協商快取 |
四. 總結:
1. 只設定Cache-Control
- max-age未過期 -> 命中強快取
- max-age過期 -> 資源未修改 -> 命中協商快取
- max-age過期 -> 資源已修改 -> 伺服器獲取資源
- no-cache -> 資源未修改 -> 命中協商快取
- no-cache -> 資源已修改 -> 伺服器獲取資源
- no-store -> 伺服器獲取資源
2. 只設定Expires
- Expires未過期 -> 命中強快取
- Expires過期 -> 資源未修改 -> 命中協商快取
- Expires過期 -> 資源已修改 -> 伺服器獲取資源
3. Cache-Control
與Expires
同時存在
- 只有Cache-Control生效
4. Cache-Control
與Expires
都未設定
- 對比
request
中的If-None-Match
與response
中的ETag
If-None-Match
與ETag
相同 -> 協商快取If-None-Match
與ETag
不相同 -> 伺服器獲取資源- 感覺實際上就是走的
Cache-Control no-cahce