看了這篇文章,瀏覽器快取一定能記住

RaKL發表於2020-04-05

關於瀏覽器快取方面的知識,也看了好幾篇文章了。大家提到也總能說幾個關鍵詞出來,但是說到如何使用可能又不夠確定了。因此我在這通過實操記錄下來,方便更好的理解和記憶。

首先,常說的瀏覽器快取的兩種情況強快取協商快取,優先順序較高的是強快取,當強快取命中失敗時才會走協商快取。

強快取

強快取是不需要傳送http請求的,當資源命中強快取時,直接從快取中獲取,響應狀態返回200,開啟控制檯檢視Size也不顯示資源大小,而是告訴我們來自快取。

強快取

強快取

強快取如何實現呢,本文後端程式碼都通過Koa來演示

1、Expires

設定過期時間,第一次請求以後響應頭中設定Expires

router.get('/img/1.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3))
    })
    ctx.body = file
})
複製程式碼

將過期時間設定為三天後,這樣在快取未過期下就不需要再重新請求服務端了。但是Expires存在問題,就是客戶端的時間和服務端時間可能是不一致的,比如你把電腦時間設定成三天甚至一年後,快取就無效了。

2、Cache-Control

HTTP1.1新增了Cache-Control欄位,Cache-Control優先順序高於Expires,兩者同時使用時會忽略Expires(Expires保留的作用是向下相容),Cache-Control欄位屬性值比較靈活。

2.1 max-age

max-age指定資源的有效時間,單位是秒。以下是demo2,Cache-controla:max-age=0,同時設定Expires。測試可以看到圖片始終不會快取,也體現出了優先順序的問題。

// demo2
router.get('/img/2.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3)),
        'Cache-control': 'max-age=0'
    })
    ctx.body = file
})
複製程式碼

再測試demo3,這樣圖片的快取時間就是10秒

// demo3
router.get('/img/3.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Cache-control': 'max-age=10'
    })
    ctx.body = file
})
複製程式碼

2.2 s-maxage

s-maxagemax-age類似,不同的地方s-maxage是針對代理伺服器的

// demo4
router.get('/img/4.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() - 1000*60*60*24*3)),
        'Cache-control': 's-maxage=10'
    })
    ctx.body = file
})
複製程式碼

測試發現圖片無法快取

2.3 private和public

和前面兩個屬性相關,private對應資源可以被瀏覽器快取,public表示資源既可以被瀏覽器快取,也可以被代理伺服器快取。 預設值是private,相當於設定了max-age的情況;當設定了s-maxage屬性,就表示可以被代理伺服器快取,也就是等同於設定成public

這裡請看demo5

// demo5
router.get('/img/5.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Cache-control': 'private'
    })
    ctx.body = file
})
複製程式碼

測試發現圖片始終不會快取,所以private對應max-age的預設值應該是0。但是private真的和max-age=0完全相同嗎,我又寫了個栗子測試。

//demo6
router.get('/img/6.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    ctx.set({
        'Expires': new Date((+new Date() + 1000*60*60*24*3)),
        'Cache-control': 'private'
    })
    ctx.body = file
})
複製程式碼

測試發現圖片會被快取,說明private情況下,不會讓Expires失效。

2.4 no-store和no-cache

no-store比較暴力,不適用任何快取機制,直接向伺服器發起請求,下載完整資源。

no-cache跳過強快取,也就是Expiresmax-age等都無效了,直接請求伺服器,確認資源是否過期,也就是進入協商快取的階段。協商快取中有兩個關鍵欄位,Last-ModifiedEtag

協商快取

1、Last-Modified和If-Modified-Since

請看demo8

// demo8
router.get('/img/8.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    const stats = fs.statSync(path.resolve(__dirname,`.${ctx.request.path}`))
    if(ctx.request.header['if-modified-since'] === stats.mtime.toUTCString()){
        return ctx.status = 304
    }
    ctx.set({
        'Cache-control': 'no-cache',
        'Last-Modified': stats.mtime.toUTCString()
    })
    ctx.body = file
})
複製程式碼

第一次請求後,在響應頭中加入Last-Modified欄位,返回資源的最後修改時間,下次請求時客戶端請求頭會帶上If-Modified-Since的欄位,裡面的值就是之前響應頭中Last-Modified的值,然後進行比較,如果沒有變化,則返回304的狀態碼。

協商快取

如果改造一下把'Cache-control': 'no-cache'去掉呢,測試後發現,在不清快取的情況資源就變成強快取了,請求頭中的If-Modified-Since也沒有了(加上也沒用,因為不會傳送http請求),導致檔案更新就無法檢測了。

2、 ETag和If-None-Match

請看demo9

// demo9
router.get('/img/9.jpg', ctx => {
    const file = fs.readFileSync(path.resolve(__dirname,`.${ctx.request.path}`))
    const stats = fs.statSync(path.resolve(__dirname,`.${ctx.request.path}`))
    if(ctx.request.header['if-none-match']){
        if(ctx.request.header['if-none-match'] === 'abc123'){
            return ctx.status = 304
        }
    }else if(ctx.request.header['if-modified-since'] === stats.mtime.toUTCString()){
        return ctx.status = 304
    }
    ctx.set({
        'Cache-control': 'no-cache',
        'Last-Modified': stats.mtime.toUTCString(),
        'ETag': 'abc123'
    })
    ctx.body = file
})
複製程式碼

Last-Modified類似,第一次請求以後,響應頭中會增加ETag欄位,ETag通過資源的內容生成一個識別符號,下次請求在請求中增加If-None-Match欄位,值就是之前響應頭中ETag的值,然後進行比較。ETag可以說是對Last-Modified的一個補充,因為Last-Modified也是有不足的地方。舉個例子,Last-Modified中的時間是精確到秒的,如果同一秒內檔案被修改了一次,下一次請求時,預期獲取新資源而實際還是會走協商快取。而ETag是基於資源內容的,所以會生成新的值,因此能達到預期效果。

補充:瀏覽器快取的四個位置

  1. Service Worker Cache
  2. Memory Cache
  3. Disk Cache
  4. Push Cache

Service Worker Cache,這個很多人都聽過,是PWA應用的重要實現機制,推薦資源:PWA應用實戰

Memory CacheDisk Cache就是前面總結的,我們強快取和協商快取存放資源的位置。Memory Cache記憶體快取,是效率最高的,當然記憶體資源也是昂貴的有限的,不可能都使用記憶體快取,Disk Cache磁碟快取,相對來說讀取速度慢些。一般來說,大檔案或者記憶體使用高的情況下,資源會被丟進磁碟。

Push Cache推送快取,快取的最後一道防線。是HTTP2中的內容,需要自行去了解。

總結幾個點

1、協商快取中有兩組約定的欄位,一是Last-ModifiedIf-Modified-Since;二是ETagIf-None-Match。也就是響應頭中存在Last-Modified(或ETag),則下次請求的響應頭中會自動增加If-Modified-Since(或If-None-Match)欄位,至於是否走協商快取取決於具體程式碼,比如觸發條件一般就是比較同一組資料是否相同,同時因為第二組更加準確,所以優先順序也更高。

2、Cache-Control: privateCache-Control: max-age=0,效果不完全相同,前者不會讓Expires失效。

3、瀏覽器快取機制總覽,首先如果命中強快取就直接使用;否則就進入協商快取階段,這裡會產生http請求,通過協商快取的兩組規範,檢查資源是否更新。沒更新的話就返回304狀態碼,否則就重新獲取資源並返回200狀態碼。

4、通過上面的演示可以看得出來,處理快取的工作量主要在後端,然後在工作中,靜態資源我們一般都是直接使用中介軟體來處理,比如筆者在Koa專案中的話,會用koa-static這個中介軟體。前端不需要寫相關程式碼,後端用現成的輪子,因此快取相關的知識就被拋棄了。

5、看完這些demo,相信你一定能記住,demo地址,也可以自己clone下來跑幾遍試試,希望本文對你有幫助。

相關文章