先看一張經典的流程圖,結合理解html
吃了它~前端
數據傳輸
,節省了網費。性能
速度
主要就是在瀏覽器本地把對應的 IP 和域名關聯起來,這樣在進行 DNS 解析的時候就很快。git
是指存在內存中的緩存。從優先級上來講,它是瀏覽器最早嘗試去命中的一種緩存。從效率上來講,它是響應速度最快的一種緩存。 內存緩存是快的,也是「短命」的。它和渲染進程「生死相依」,當進程結束後,也就是 tab 關閉之後,內存裏的數據也將不復存在。github
瀏覽器緩存,也稱Http 緩存,分爲強緩存和協商緩存。優先級較高的是強緩存,在命中強緩存失敗的狀況下,纔會走協商緩存瀏覽器
強緩存是利用 http
頭中的 Expires
和 Cache-Control
兩個字段來控制的。強緩存中,當請求再次發出時,瀏覽器會根據其中的 Expires
和 cache-control
判斷目標資源是否「命中」強緩存,若命中則直接從緩存中獲取資源,不會再與服務端發生通訊。緩存
實現強緩存,過去咱們一直用Expires
。當服務器返回響應時,在 Response Headers 中將過時時間寫入 Expires
字段。像這樣服務器
expires: Wed, 12 Sep 2019 06:12:18 GMT
markdown
能夠看到,expires
是一個時間戳,接下來若是咱們試圖再次向服務器請求資源,瀏覽器就會先對比本地時間和 expires
的時間戳,若是本地時間小於 expires
設定的過時時間,那麼就直接去緩存中取這個資源。網絡
從這樣的描述中你們也不難猜想,expires
是有問題的,它最大的問題在於對本地時間
的依賴。若是服務端和客戶端的時間設置可能不一樣,或者我直接手動去把客戶端的時間改掉,那麼 expires
將沒法達到咱們的預期。session
考慮到 expires
的侷限性,HTTP1.1
新增了Cache-Control
字段來完成 expires
的任務。expires
能作的事情,Cache-Control
都能作;expires
完成不了的事情,Cache-Control
也能作。所以,Cache-Control
能夠視做是 expires
的徹底替代方案。在當下的前端實踐裏,咱們繼續使用 expires
的惟一目的就是向下兼容。
在 Cache-Control
中,咱們經過max-age
來控制資源的有效期。max-age
不是一個時間戳,而是一個時間長度。在本例中,max-age
是 31536000
秒,它意味着該資源在 31536000
秒之內都是有效的,完美地規避了時間戳帶來的潛在問題。
Cache-Control
相對於 expires 更加準確,它的優先級也更高。當 Cache-Control
與 expires 同時出現時,咱們以 Cache-Control
爲準。
能夠參考下下面兩張圖:
協商緩存依賴於服務端與瀏覽器之間的通訊。協商緩存機制下,瀏覽器須要向服務器去詢問緩存的相關信息,進而判斷是從新發起請求、下載完整的響應,仍是從本地獲取緩存的資源。若是服務端提示緩存資源未改動(Not Modified),資源會被重定向到瀏覽器緩存,這種狀況下網絡請求對應的狀態碼是 304。
協商緩存的實現,從 Last-Modified
到 Etag
,Last-Modified
是一個時間戳,若是咱們啓用了協商緩存,它會在首次請求時隨着 Response Headers
返回:
Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT
複製代碼
隨後咱們每次請求時,瀏覽器的請求頭 headers
會帶上一個叫 If-Modified-Since
的時間戳字段,它的值正是上一次 response
返回給它的 Last-Modified
值:
If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT
複製代碼
服務器接收到這個時間戳後,會比對該時間戳和資源在服務器上的最後修改時間是否一致,從而判斷資源是否發生了變化。若是發生了變化,就會返回一個完整的響應內容,並在 Response Headers
中添加新的 Last-Modified
值;不然,返回 304 響應,Response Headers
不會再添加 Last-Modified
字段。
以下圖:
經過最後修改時間來判斷緩存是否可用
Last-Modified
:響應時告訴客戶端此資源的最後修改時間If-Modified-Since
:當資源過時時(使用 Cache-Control
標識的 max-age
),發現資源具備 Last-Modified
聲明,則再次向服務器請求時帶上頭 If-Modified-Since
。If-Modified-Since
則與被請求資源的最後修改時間進行比對。若最後修改時間較新,說明資源又被改動過,則響應最新的資源內容並返回 200
狀態碼;If-Modified-Since
同樣,說明資源沒有修改,則響應 304
表示未更新,告知瀏覽器繼續使用所保存的緩存文件。看個實例代碼:
let http = require('http'); let fs = require('fs'); let path = require('path'); let mime = require('mime'); http.createServer(function (req, res) { let file = path.join(__dirname, req.url); fs.stat(file, (err, stat) => { if (err) { sendError(err, req, res, file, stat); } else { let ifModifiedSince = req.headers['if-modified-since']; if (ifModifiedSince) { if (ifModifiedSince == stat.ctime.toGMTString()) { res.writeHead(304); res.end(); } else { send(req, res, file, stat); } } else { send(req, res, file, stat); } } }); }).listen(8080); function send(req, res, file, stat) { res.setHeader('Last-Modified', stat.ctime.toGMTString()); res.writeHead(200, { 'Content-Type': mime.getType(file) }); fs.createReadStream(file).pipe(res); } function sendError(err, req, res, file, stat) { res.writeHead(400, { "Content-Type": 'text/html' }); res.end(err ? err.toString() : "Not Found"); 複製代碼
使用 Last-Modified 存在一些弊端,這其中最多見的就是這樣幾個場景 1. 某些服務器不能精確獲得文件的最後修改時間, 這樣就沒法經過最後修改時間來判斷文件是否更新了。 2. 咱們編輯了文件,但文件的內容沒有改變。服務端並不清楚咱們是否真正改變了文件,它仍然經過最後編輯時間進行判斷。所以這個資源在再次被請求時,會被當作新資源,進而引起一次完整的響應——不應從新請求的時候,也會從新請求。 3. 當咱們修改文件的速度過快時(好比花了 100ms
完成了改動),因爲 If-Modified-Since
只能檢查到以秒爲最小計量單位的時間差,因此它是感知不到這個改動的——該從新請求的時候,反而沒有從新請求了。 4. 若是一樣的一個文件位於多個CDN服務器上的時候內容雖然同樣,修改時間不同。
第二和第三這兩個場景其實指向了同一個 bug——服務器並無正確感知文件的變化。爲了解決這樣的問題,Etag 做爲 Last-Modified
的補充出現了。
這個是協商緩存中的另一種
Etag
是由服務器爲每一個資源生成的惟一的標識字符串(指紋),這個標識字符串能夠是基於文件內容編碼的,只要文件內容不一樣,它們對應的 Etag
就是不一樣的,反之亦然。所以 Etag
可以精準地感知文件的變化。
Etag
是 Web 服務端產生的,而後發給瀏覽器客戶端。生成過程須要服務器額外付出開銷,會影響服務端的性能,這是它的弊端。所以啓用 Etag
須要咱們審時度勢。正如咱們剛剛所提到的——Etag
並不能替代 Last-Modified
,它只能做爲 Last-Modified
的補充和強化存在。
執行流程是這樣的: 1. 客戶端想判斷緩存是否可用能夠先獲取緩存中文檔的ETag
,而後經過If-None-Match
發送請求給 Web 服務器詢問此緩存是否可用。 2. 服務器收到請求,將服務器的中此文件的ETag
,跟請求頭中的If-None-Match
相比較,若是值是同樣的,說明緩存仍是最新的,Web 服務器將發送304 Not Modified
響應碼給客戶端表示緩存未修改過,可使用。 3. 若是不同則 Web 服務器將發送該文檔的最新版本給瀏覽器客戶端
看以下實例代碼:
let http = require("http"); let fs = require("fs"); let path = require("path"); let mime = require("mime"); let crypto = require("crypto"); http .createServer(function(req, res) { let file = path.join(__dirname, req.url); fs.stat(file, (err, stat) => { if (err) { sendError(err, req, res, file, stat); } else { let ifNoneMatch = req.headers["if-none-match"]; let etag = crypto .createHash("sha1") .update(stat.ctime.toGMTString() + stat.size) .digest("hex"); if (ifNoneMatch) { if (ifNoneMatch == etag) { res.writeHead(304); res.end(); } else { send(req, res, file, etag); } } else { send(req, res, file, etag); } } }); }) .listen(8080); function send(req, res, file, etag) { res.setHeader("ETag", etag); res.writeHead(200, { "Content-Type": mime.lookup(file) }); fs.createReadStream(file).pipe(res); } function sendError(err, req, res, file, etag) { res.writeHead(400, { "Content-Type": "text/html" }); res.end(err ? err.toString() : "Not Found"); } 複製代碼
優先級:
Etag
在感知文件變化上比 Last-Modified
更加準確,優先級也更高。當 Etag
和 Last-Modified
同時存在時,以 Etag
爲準。
對比:
Service Worker
是一種獨立於主線程以外的 Javascript 線程
。它脫離於瀏覽器窗體,所以沒法直接訪問 DOM。這樣獨立的個性使得
Service Worker
的「我的行爲」沒法干擾頁面的性能,這個幕後工做者能夠幫咱們實現離線緩存、消息推送和網絡代理等功能。咱們藉助 Service worker
實現的離線緩存就稱爲 Service Worker
Cache。
Service Worker
的生命週期包括 install、activited、working
三個階段。一旦 Service Worker
被 install,它將始終存在,只會在 active 與 working 之間切換,除非咱們主動終止它。這是它能夠用來實現離線存儲的重要先決條件.
它就在瀏覽器開發工具(F12) Application
標籤頁中
Push Cache
是指 HTTP2
在 server push
階段存在的緩存。這塊的知識比較新,應用也還處於萌芽階段,應用範圍有限不表明不重要——HTTP2
是趨勢、是將來。在它還未被推而廣之的此時此刻,仍但願你們能對 Push Cache
的關鍵特性有所瞭解:
Push Cache
是緩存的最後一道防線。瀏覽器只有在 Memory Cache
、HTTP Cache
和 Service Worker Cache
均未命中的狀況下才會去詢問 Push Cache
。Push Cache
是一種存在於會話階段的緩存,當 session 終止時,緩存也隨之釋放。HTTP2
鏈接,那麼它們就能夠共享同一個 Push Cache
。走上面的緩存機制
Cache
目錄,第二次請求時瀏覽器會先檢查Cache
目錄下是否含有該文件,若是有,而且還沒到Expires
設置的時間,即文件尚未過時,那麼此時瀏覽器將直接從 Cache 目錄中讀取文件,而再也不發送請求Expires
是服務器響應消息頭字段,在響應 http 請求時告訴瀏覽器在過時時間前瀏覽器能夠直接從瀏覽器緩存取數據,而無需再次請求,這是HTTP1.0
的內容,如今瀏覽器均默認使用HTTP1.1
,因此基本能夠忽略Cache-Control
與Expires
的做用一致,都是指明當前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數據仍是從新發請求到服務器取數據,若是同時設置的話,其優先級高於Expires
let http = require("http"); let fs = require("fs"); let path = require("path"); let mime = require("mime"); let crypto = require("crypto"); http .createServer(function(req, res) { let file = path.join(__dirname, req.url); console.log(file); fs.stat(file, (err, stat) => { if (err) { sendError(err, req, res, file, stat); } else { send(req, res, file); } }); }) .listen(8080); function send(req, res, file) { let expires = new Date(Date.now() + 60 * 1000); res.setHeader("Expires", expires.toUTCString()); res.setHeader("Cache-Control", "max-age=60"); res.writeHead(200, { "Content-Type": mime.lookup(file) }); fs.createReadStream(file).pipe(res); } function sendError(err, req, res, file, etag) { res.writeHead(400, { "Content-Type": "text/html" }); res.end(err ? err.toString() : "Not Found"); } 複製代碼
若是本文對你有幫助的話,給本文點個贊吧
鄙人github,一塊兒學習~