整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          十分全面!作為前端應當了解的那些Web緩存知識

          存優點

          通常所說的Web緩存指的是可以自動保存常見http請求副本的http設備。對于前端開發者來說,瀏覽器充當了重要角色。除此外常見的還有各種各樣的代理服務器也可以做緩存。當Web請求到達緩存時,緩存從本地副本`本地副本`中提取這個副本內容而不需要經過服務器。這帶來了以下優點:

          • 緩存減少了冗余的數據傳輸,節省流量

          • 緩存緩解了帶寬瓶頸問題。不需要更多的帶寬就能更快加載頁面

          • 緩存緩解了瞬間擁塞,降低了對原始服務器的要求。

          • 緩存降低了距離延時, 因為從較遠的地方加載頁面會更慢一些。

          緩存種類

          緩存可以是單個用戶專用的,也可以是多個用戶共享的。專用緩存被稱為私有緩存,共享的緩存被稱為公有緩存。

          私有緩存

          私有緩存只針對專有用戶,所以不需要很大空間,廉價。Web瀏覽器中有內建的私有緩存——大多數瀏覽器都會將常用資源緩存在你的個人電腦的磁盤和內存中。如Chrome瀏覽器的緩存存放位置就在:C:\Users\Your_Account\AppData\Local\Google\Chrome\User Data\Default中的Cache文件夾和Media Cache文件夾。

          公有緩存

          公有緩存是特殊的共享代理服務器,被稱為緩存代理服務器或代理緩存(反向代理的一種用途)。公有緩存會接受來自多個用戶的訪問,所以通過它能夠更好的減少冗余流量。

          下圖中每個客戶端都會重復的向服務器訪問一個資源(此時還不在私有緩存中),這樣它會多次訪問服務器,增加服務器壓力。而使用共享的公有緩存時,緩存只需要從服務器取一次,以后不用再經過服務器,能夠顯著減輕服務器壓力。

          事實上在實際應用中通常采用層次化的公有緩存,基本思想是在靠近客戶端的地方使用小型廉價緩存,而更高層次中,則逐步采用更大、功能更強的緩存在裝載多用戶共享的資源。

          緩存處理流程

          而對于前端開發者來說,我們主要跟瀏覽器中的緩存打交道,所以上圖流程簡化為:

          下面這張圖展示了某一網站,對不同資源的請求結果,其中可以看到有的資源直接從緩存中讀取,有的資源跟服務器進行了再驗證,有的資源重新從服務器端獲取。

          注意,我們討論的所有關于緩存資源的問題,都僅僅針對GET請求。而對于POST, DELETE, PUT這類行為性操作通常不做任何緩存

          新鮮度限值

          HTTP通過緩存將服務器資源的副本保留一段時間,這段時間稱為新鮮度限值。這在一段時間內請求相同資源不會再通過服務器。HTTP協議中Cache-Control 和 Expires可以用來設置新鮮度的限值,前者是HTTP1.1中新增的響應頭,后者是HTTP1.0中的響應頭。二者所做的事時都是相同的,但由于Cache-Control使用的是相對時間,而Expires可能存在客戶端與服務器端時間不一樣的問題,所以我們更傾向于選擇Cache-Control。

          Cache-Control

          下面我們來看看Cache-Control都可以設置哪些屬性值:

          max-age(單位為s)指定設置緩存最大的有效時間,定義的是時間長短。當瀏覽器向服務器發送請求后,在max-age這段時間里瀏覽器就不會再向服務器發送請求了。

          <html>

          <head>

          <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

          <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

          <meta http-equiv="X-UA-Compatible" content="IE=EDGE" />

          <title>Web Cache</title>

          <link rel="shortcut icon" href="./shortcut.png">

          <script>

          </script>

          </head>

          <body class="claro">

          <img src="./cache.png">

          </body>

          </html>

          var http = require('http');

          var fs = require('fs');

          http.createServer(function(req, res) {

          if (req.url === '/' || req.url === '' || req.url === '/index.html') {

          fs.readFile('./index.html', function(err, file) {

          console.log(req.url)

          //對主文檔設置緩存,無效果

          res.setHeader('Cache-Control', "no-cache, max-age=" + 5);

          res.setHeader('Content-Type', 'text/html');

          res.writeHead('200', "OK");

          res.end(file);

          });

          }

          if (req.url === '/cache.png') {

          fs.readFile('./cache.png', function(err, file) {

          res.setHeader('Cache-Control', "max-age=" + 5);//緩存五秒

          res.setHeader('Content-Type', 'images/png');

          res.writeHead('200', "Not Modified");

          res.end(file);

          });

          }

          }).listen(8888)

          當在5秒內第二次訪問頁面時,瀏覽器會直接從緩存中取得資源

          • public 指定響應可以在代理緩存中被緩存,于是可以被多用戶共享。如果沒有明確指定private,則默認為public。

          • private 響應只能在私有緩存中被緩存,不能放在代理緩存上。對一些用戶信息敏感的資源,通常需要設置為private。

          • no-cache 表示必須先與服務器確認資源是否被更改過(依靠If-None-Match和Etag),然后再決定是否使用本地緩存。

          如果上文中關于cache.png的處理改成下面這樣,則每次訪問頁面,瀏覽器都需要先去服務器端驗證資源有沒有被更改。

          fs.readFile('./cache.png', function(err, file) {

          console.log(req.headers);

          console.log(req.url)

          if (!req.headers['if-none-match']) {

          res.setHeader('Cache-Control', "no-cache, max-age=" + 5);

          res.setHeader('Content-Type', 'images/png');

          res.setHeader('Etag', "ffff");

          res.writeHead('200', "Not Modified");

          res.end(file);

          } else {

          if (req.headers['if-none-match'] === 'ffff') {

          res.writeHead('304', "Not Modified");

          res.end();

          } else {

          res.setHeader('Cache-Control', "max-age=" + 5);

          res.setHeader('Content-Type', 'images/png');

          res.setHeader('Etag', "ffff");

          res.writeHead('200', "Not Modified");

          res.end(file);

          }

          }

          });

          no-store 絕對禁止緩存任何資源,也就是說每次用戶請求資源時,都會向服務器發送一個請求,每次都會下載完整的資源。通常用于機密性資源。

          關于Cache-Control的使用,見下面這張圖(來自大額)

          客戶端的新鮮度限值

          Cache-Control不僅僅可以在響應頭中設置,還可以在請求頭中設置。瀏覽器通過請求頭中設置Cache-Control可以決定是否從緩存中讀取資源。*這也是為什么有時候點擊瀏覽器刷新按鈕和在地址欄回車,在NetWork模塊中看到完全不同的結果*

          Expires

          不推薦使用Expires,它指定的是具體的過期日期而不是秒數。因為很多服務器跟客戶端存在時鐘不一致的情況,所以最好還是使用Cache-Control.

          服務器再驗證

          瀏覽器或代理緩存中緩存的資源過期了,并不意味著它和原始服務器上的資源有實際的差異,僅僅意味著到了要進行核對的時間了。這種情況被稱為服務器再驗證。

          • 如果資源發生變化,則需要取得新的資源,并在緩存中替換舊資源。

          • 如果資源沒有發生變化,緩存只需要獲取新的響應頭,和一個新的過期時間,對緩存中的資源過期時間進行更新即可。 HTTP1.1推薦使用的驗證方式是If-None-Match/Etag,在HTTP1.0中則使用If-Modified-Since/Last-Modified。

          Etag與If-None-Match

          根據實體內容生成一段hash字符串,標識資源的狀態,由服務端產生。瀏覽器會將這串字符串傳回服務器,驗證資源是否已經修改,如果沒有修改,過程如下(圖片來自淺談Web緩存):

          上文的demo中我們見到過服務器端如何驗證Etag:

          由于Etag有服務器構造,所以在集群環境中一定要保證Etag的唯一性

          If-Modified-Since與Last-Modified

          這兩個是HTTP1.0中用來驗證資源是否過期的請求/響應頭,這兩個頭部都是日期,驗證過程與Etag類似,這里不詳細介紹。使用這兩個頭部來驗證資源是否更新時,存在以下問題:

          • 有些文檔資源周期性的被重寫,但實際內容沒有改變。此時文件元數據中會顯示文件最近的修改日期與If-Modified-Since不相同,導致不必要的響應。

          • 有些文檔資源被修改了,但修改內容并不重要,不需要所有的緩存都更新(比如代碼注釋)

          關于緩存的更新問題,請大家看看這里張云龍的回答,本文就不詳細展開了。

          本文demo代碼如下:

          <!DOCTYPE HTML>

          <html>

          <head>

          <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

          <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

          <meta http-equiv="X-UA-Compatible" content="IE=EDGE" />

          <title>Web Cache</title>

          <link rel="shortcut icon" href="./shortcut.png">

          <script>

          </script>

          </head>

          <body class="claro">

          <img src="./cache.png">

          </body>

          </html>

          var http = require('http');

          var fs = require('fs');

          http.createServer(function(req, res) {

          if (req.url === '/' || req.url === '' || req.url === '/index.html') {

          fs.readFile('./index.html', function(err, file) {

          console.log(req.url)

          //對主文檔設置緩存,無效果

          res.setHeader('Cache-Control', "no-cache, max-age=" + 5);

          res.setHeader('Content-Type', 'text/html');

          res.writeHead('200', "OK");

          res.end(file);

          });

          }

          if (req.url === '/shortcut.png') {

          fs.readFile('./shortcut.png', function(err, file) {

          console.log(req.url)

          res.setHeader('Content-Type', 'images/png');

          res.writeHead('200', "OK");

          res.end(file);

          })

          }

          if (req.url === '/cache.png') {

          fs.readFile('./cache.png', function(err, file) {

          console.log(req.headers);

          console.log(req.url)

          if (!req.headers['if-none-match']) {

          res.setHeader('Cache-Control', "max-age=" + 5);

          res.setHeader('Content-Type', 'images/png');

          res.setHeader('Etag', "ffff");

          res.writeHead('200', "Not Modified");

          res.end(file);

          } else {

          if (req.headers['if-none-match'] === 'ffff') {

          res.writeHead('304', "Not Modified");

          res.end();

          } else {

          res.setHeader('Cache-Control', "max-age=" + 5);

          res.setHeader('Content-Type', 'images/png');

          res.setHeader('Etag', "ffff");

          res.writeHead('200', "Not Modified");

          res.end(file);

          }

          }

          });

          }

          }).listen(8888)

          更多深度技術內容,請關注云棲社區微信公眾號:yunqiinsight。

          家都知道緩存的英文叫做 cache。但我發現一個有趣的現象:這個單詞在不同人的口中有不同的讀音。為了全面了解緩存,我們得先從讀音開始,這樣才能夠在和其他同事(例如 PM)交(zhuāng)流(bī)時體現自己的修(bī)養(gé)。cache 的發音是 /k??/(同 cash),這也是一個廣泛認可的發音。

          前端緩存/后端緩存

          扯了些沒用的,我們先進入定義環節:什么是前端緩存?與之相對的什么又是后端緩存?

          基本的網絡請求就是三個步驟:請求,處理,響應。

          后端緩存主要集中于“處理”步驟,通過保留數據庫連接,存儲處理結果等方式縮短處理時間,盡快進入“響應”步驟。當然這不在本文的討論范圍之內。

          而前端緩存則可以在剩下的兩步:“請求”和“響應”中進行。在“請求”步驟中,瀏覽器也可以通過存儲結果的方式直接使用資源,直接省去了發送請求;而“響應”步驟需要瀏覽器和服務器共同配合,通過減少響應內容來縮短傳輸時間。這些都會在下面進行討論。

          本文主要包含

          • 按緩存位置分類 (memory cache, disk cache, Service Worker 等)
          • 按失效策略分類 (Cache-Control, ETag 等)
          • 幫助理解原理的一些案例
          • 緩存的應用模式

          按緩存位置分類

          我看過的大部分討論緩存的文章會直接從 HTTP 協議頭中的緩存字段開始,例如 Cache-Control, ETag, max-age 等。但偶爾也會聽到別人討論 memory cache, disk cache 等。那這兩種分類體系究竟有何關聯?是否有交叉?(我個人認為這是本文的最大價值所在,因為在寫之前我自己也是被兩種分類體系搞的一團糟)

          實際上,HTTP 協議頭的那些字段,都屬于 disk cache 的范疇,是幾個緩存位置的其中之一。因此本著從全局到局部的原則,我們應當先從緩存位置開始討論。等講到 disk cache 時,才會詳細講述這些協議頭的字段及其作用。

          我們可以在 Chrome 的開發者工具中,Network -> Size 一列看到一個請求最終的處理方式:如果是大小 (多少 K, 多少 M 等) 就表示是網絡請求,否則會列出 from memory cache, from disk cache 和 from ServiceWorker。

          它們的優先級是:(由上到下尋找,找到即返回;找不到則繼續)

          1. Service Worker
          2. Memory Cache
          3. Disk Cache
          4. 網絡請求

          memory cache

          memory cache 是內存中的緩存,(與之相對 disk cache 就是硬盤上的緩存)。按照操作系統的常理:先讀內存,再讀硬盤。disk cache 將在后面介紹 (因為它的優先級更低一些),這里先討論 memory cache。

          幾乎所有的網絡請求資源都會被瀏覽器自動加入到 memory cache 中。但是也正因為數量很大但是瀏覽器占用的內存不能無限擴大這樣兩個因素,memory cache 注定只能是個“短期存儲”。常規情況下,瀏覽器的 TAB 關閉后該次瀏覽的 memory cache 便告失效 (為了給其他 TAB 騰出位置)。而如果極端情況下 (例如一個頁面的緩存就占用了超級多的內存),那可能在 TAB 沒關閉之前,排在前面的緩存就已經失效了。

          剛才提過,幾乎所有的請求資源 都能進入 memory cache,這里細分一下主要有兩塊:

          1. preloader。如果你對這個機制不太了解,這里做一個簡單的介紹,詳情可以參閱這篇文章。
          2. 熟悉瀏覽器處理流程的同學們應該了解,在瀏覽器打開網頁的過程中,會先請求 HTML 然后解析。之后如果瀏覽器發現了 js, css 等需要解析和執行的資源時,它會使用 CPU 資源對它們進行解析和執行。在古老的年代(大約 2007 年以前),“請求 js/css - 解析執行 - 請求下一個 js/css - 解析執行下一個 js/css” 這樣的“串行”操作模式在每次打開頁面之前進行著。很明顯在解析執行的時候,網絡請求是空閑的,這就有了發揮的空間:我們能不能一邊解析執行 js/css,一邊去請求下一個(或下一批)資源呢?
          3. 這就是 preloader 要做的事情。不過 preloader 沒有一個官方標準,所以每個瀏覽器的處理都略有區別。例如有些瀏覽器還會下載 css 中的 @import 內容或者 <video> 的 poster等。
          4. 而這些被 preloader 請求夠來的資源就會被放入 memory cache 中,供之后的解析執行操作使用。
          5. preload (雖然看上去和剛才的 preloader 就差了倆字母)。實際上這個大家應該更加熟悉一些,例如 <link rel="preload">。這些顯式指定的預加載資源,也會被放入 memory cache 中。

          memory cache 機制保證了一個頁面中如果有兩個相同的請求 (例如兩個 src 相同的 <img>,兩個 href 相同的 <link>)都實際只會被請求最多一次,避免浪費。

          不過在匹配緩存時,除了匹配完全相同的 URL 之外,還會比對他們的類型,CORS 中的域名規則等。因此一個作為腳本 (script) 類型被緩存的資源是不能用在圖片 (image) 類型的請求中的,即便他們 src 相等。

          在從 memory cache 獲取緩存內容時,瀏覽器會忽視例如 max-age=0, no-cache 等頭部配置。例如頁面上存在幾個相同 src 的圖片,即便它們可能被設置為不緩存,但依然會從 memory cache 中讀取。這是因為 memory cache 只是短期使用,大部分情況生命周期只有一次瀏覽而已。而 max-age=0 在語義上普遍被解讀為“不要在下次瀏覽時使用”,所以和 memory cache 并不沖突。

          但如果站長是真心不想讓一個資源進入緩存,就連短期也不行,那就需要使用 no-store。存在這個頭部配置的話,即便是 memory cache 也不會存儲,自然也不會從中讀取了。(后面的第二個示例有關于這點的體現)

          disk cache

          disk cache 也叫 HTTP cache,顧名思義是存儲在硬盤上的緩存,因此它是持久存儲的,是實際存在于文件系統中的。而且它允許相同的資源在跨會話,甚至跨站點的情況下使用,例如兩個站點都使用了同一張圖片。

          disk cache 會嚴格根據 HTTP 頭信息中的各類字段來判定哪些資源可以緩存,哪些資源不可以緩存;哪些資源是仍然可用的,哪些資源是過時需要重新請求的。當命中緩存之后,瀏覽器會從硬盤中讀取資源,雖然比起從內存中讀取慢了一些,但比起網絡請求還是快了不少的。絕大部分的緩存都來自 disk cache。

          關于 HTTP 的協議頭中的緩存字段,我們會在稍后進行詳細討論。

          凡是持久性存儲都會面臨容量增長的問題,disk cache 也不例外。在瀏覽器自動清理時,會有神秘的算法去把“最老的”或者“最可能過時的”資源刪除,因此是一個一個刪除的。不過每個瀏覽器識別“最老的”和“最可能過時的”資源的算法不盡相同,可能也是它們差異性的體現。

          Service Worker

          上述的緩存策略以及緩存/讀取/失效的動作都是由瀏覽器內部判斷 & 進行的,我們只能設置響應頭的某些字段來告訴瀏覽器,而不能自己操作。舉個生活中去銀行存/取錢的例子來說,你只能告訴銀行職員,我要存/取多少錢,然后把由他們會經過一系列的記錄和手續之后,把錢放到金庫中去,或者從金庫中取出錢來交給你。

          但 Service Worker 的出現,給予了我們另外一種更加靈活,更加直接的操作方式。依然以存/取錢為例,我們現在可以繞開銀行職員,自己走到金庫前(當然是有別于上述金庫的一個單獨的小金庫),自己把錢放進去或者取出來。因此我們可以選擇放哪些錢(緩存哪些文件),什么情況把錢取出來(路由匹配規則),取哪些錢出來(緩存匹配并返回)。當然現實中銀行沒有給我們開放這樣的服務。

          Service Worker 能夠操作的緩存是有別于瀏覽器內部的 memory cache 或者 disk cache 的。我們可以從 Chrome 的 F12 中,Application -> Cache Storage 找到這個單獨的“小金庫”。除了位置不同之外,這個緩存是永久性的,即關閉 TAB 或者瀏覽器,下次打開依然還在(而 memory cache 不是)。有兩種情況會導致這個緩存中的資源被清除:手動調用 API cache.delete(resource) 或者容量超過限制,被瀏覽器全部清空。

          如果 Service Worker 沒能命中緩存,一般情況會使用 fetch() 方法繼續獲取資源。這時候,瀏覽器就去 memory cache 或者 disk cache 進行下一次找緩存的工作了。注意:經過 Service Worker 的 fetch() 方法獲取的資源,即便它并沒有命中 Service Worker 緩存,甚至實際走了網絡請求,也會標注為 from ServiceWorker。這個情況在后面的第三個示例中有所體現。

          發送網絡請求

          如果一個請求在上述 3 個位置都沒有找到緩存,那么瀏覽器會正式發送網絡請求去獲取內容。之后容易想到,為了提升之后請求的緩存命中率,自然要把這個資源添加到緩存中去。具體來說:

          1. 根據 Service Worker 中的 handler 決定是否存入 Cache Storage (額外的緩存位置)。
          2. 根據 HTTP 頭部的相關字段(Cache-control, Pragma 等)決定是否存入 disk cache
          3. memory cache 保存一份資源 的引用,以備下次使用。

          按失效策略分類

          memory cache 是瀏覽器為了加快讀取緩存速度而進行的自身的優化行為,不受開發者控制,也不受 HTTP 協議頭的約束,算是一個黑盒。Service Worker 是由開發者編寫的額外的腳本,且緩存位置獨立,出現也較晚,使用還不算太廣泛。所以我們平時最為熟悉的其實是 disk cache,也叫 HTTP cache (因為不像 memory cache,它遵守 HTTP 協議頭中的字段)。平時所說的強制緩存,對比緩存,以及 Cache-Control 等,也都歸于此類。

          強制緩存 (也叫強緩存)

          強制緩存的含義是,當客戶端請求后,會先訪問緩存數據庫看緩存是否存在。如果存在則直接返回;不存在則請求真的服務器,響應后再寫入緩存數據庫。

          強制緩存直接減少請求數,是提升最大的緩存策略。 它的優化覆蓋了文章開頭提到過的請求數據的全部三個步驟。如果考慮使用緩存來優化網頁性能的話,強制緩存應該是首先被考慮的。

          可以造成強制緩存的字段是 Cache-control 和 Expires。

          Expires

          這是 HTTP 1.0 的字段,表示緩存到期時間,是一個絕對的時間 (當前時間+緩存時間),如

          Expires: Thu, 10 Nov 2017 08:45:11 GMT
          

          在響應消息頭中,設置這個字段之后,就可以告訴瀏覽器,在未過期之前不需要再次請求。

          但是,這個字段設置時有兩個缺點:

          1. 由于是絕對時間,用戶可能會將客戶端本地的時間進行修改,而導致瀏覽器判斷緩存失效,重新請求該資源。此外,即使不考慮自信修改,時差或者誤差等因素也可能造成客戶端與服務端的時間不一致,致使緩存失效。
          2. 寫法太復雜了。表示時間的字符串多個空格,少個字母,都會導致非法屬性從而設置失效。

          Cache-control

          已知Expires的缺點之后,在HTTP/1.1中,增加了一個字段Cache-control,該字段表示資源緩存的最大有效時間,在該時間內,客戶端不需要向服務器發送請求

          這兩者的區別就是前者是絕對時間,而后者是相對時間。如下:

          Cache-control: max-age=2592000
          

          下面列舉一些 Cache-control 字段常用的值:(完整的列表可以查看 MDN)

          • max-age:即最大有效時間,在上面的例子中我們可以看到
          • must-revalidate:如果超過了 max-age 的時間,瀏覽器必須向服務器發送請求,驗證資源是否還有效。
          • no-cache:雖然字面意思是“不要緩存”,但實際上還是要求客戶端緩存內容的,只是是否使用這個內容由后續的對比來決定。
          • no-store: 真正意義上的“不要緩存”。所有內容都不走緩存,包括強制和對比。
          • public:所有的內容都可以被緩存 (包括客戶端和代理服務器, 如 CDN)
          • private:所有的內容只有客戶端才可以緩存,代理服務器不能緩存。默認值。

          這些值可以混合使用,例如 Cache-control:public, max-age=2592000。在混合使用時,它們的優先級如下圖:

          這里有一個疑問:max-age=0 和 no-cache 等價嗎?從規范的字面意思來說,max-age 到期是 應該(SHOULD) 重新驗證,而 no-cache 是 必須(MUST) 重新驗證。但實際情況以瀏覽器實現為準,大部分情況他們倆的行為還是一致的。(如果是 max-age=0, must-revalidate 就和 no-cache 等價了)

          順帶一提,在 HTTP/1.1 之前,如果想使用 no-cache,通常是使用 Pragma 字段,如 Pragma: no-cache(這也是 Pragma 字段唯一的取值)。但是這個字段只是瀏覽器約定俗成的實現,并沒有確切規范,因此缺乏可靠性。它應該只作為一個兼容字段出現,在當前的網絡環境下其實用處已經很小。

          總結一下,自從 HTTP/1.1 開始,Expires 逐漸被 Cache-control 取代。Cache-control 是一個相對時間,即使客戶端時間發生改變,相對時間也不會隨之改變,這樣可以保持服務器和客戶端的時間一致性。而且 Cache-control 的可配置性比較強大。

          Cache-control 的優先級高于 Expires,為了兼容 HTTP/1.0 和 HTTP/1.1,實際項目中兩個字段我們都會設置。

          對比緩存 (也叫協商緩存)

          當強制緩存失效(超過規定時間)時,就需要使用對比緩存,由服務器決定緩存內容是否失效。

          流程上說,瀏覽器先請求緩存數據庫,返回一個緩存標識。之后瀏覽器拿這個標識和服務器通訊。如果緩存未失效,則返回 HTTP 狀態碼 304 表示繼續使用,于是客戶端繼續使用緩存;如果失效,則返回新的數據和緩存規則,瀏覽器響應數據后,再把規則寫入到緩存數據庫。

          對比緩存在請求數上和沒有緩存是一致的,但如果是 304 的話,返回的僅僅是一個狀態碼而已,并沒有實際的文件內容,因此 在響應體體積上的節省是它的優化點。它的優化覆蓋了文章開頭提到過的請求數據的三個步驟中的最后一個:“響應”。通過減少響應體體積,來縮短網絡傳輸時間。所以和強制緩存相比提升幅度較小,但總比沒有緩存好。

          對比緩存是可以和強制緩存一起使用的,作為在強制緩存失效后的一種后備方案。實際項目中他們也的確經常一同出現。

          對比緩存有 2 組字段(不是兩個):

          Last-Modified & If-Modified-Since

          1. 服務器通過 Last-Modified 字段告知客戶端,資源最后一次被修改的時間,例如
          Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
          
          1. 瀏覽器將這個值和內容一起記錄在緩存數據庫中。
          2. 下一次請求相同資源時時,瀏覽器從自己的緩存中找出“不確定是否過期的”緩存。因此在請求頭中將上次的 Last-Modified 的值寫入到請求頭的 If-Modified-Since 字段
          3. 服務器會將 If-Modified-Since 的值與 Last-Modified 字段進行對比。如果相等,則表示未修改,響應 304;反之,則表示修改了,響應 200 狀態碼,并返回數據。

          但是他還是有一定缺陷的:

          • 如果資源更新的速度是秒以下單位,那么該緩存是不能被使用的,因為它的時間單位最低是秒。
          • 如果文件是通過服務器動態生成的,那么該方法的更新時間永遠是生成的時間,盡管文件可能沒有變化,所以起不到緩存的作用。

          Etag & If-None-Match

          為了解決上述問題,出現了一組新的字段 Etag 和 If-None-Match

          Etag 存儲的是文件的特殊標識(一般都是 hash 生成的),服務器存儲著文件的 Etag 字段。之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新時間改變成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 變成了 If-None-Match。服務器同樣進行比較,命中返回 304, 不命中返回新資源和 200。

          Etag 的優先級高于 Last-Modified

          緩存小結

          當瀏覽器要請求資源時

          1. 調用 Service Worker 的 fetch 事件響應
          2. 查看 memory cache
          3. 查看 disk cache。這里又細分:
          4. 如果有強制緩存且未失效,則使用強制緩存,不請求服務器。這時的狀態碼全部是 200
          5. 如果有強制緩存但已失效,使用對比緩存,比較后確定 304 還是 200
          6. 發送網絡請求,等待網絡響應
          7. 把響應內容存入 disk cache (如果 HTTP 頭信息配置可以存的話)
          8. 把響應內容 的引用 存入 memory cache (無視 HTTP 頭信息的配置)
          9. 把響應內容存入 Service Worker 的 Cache Storage (如果 Service Worker 的腳本調用了 cache.put())

          一些案例

          光看原理不免枯燥。我們編寫一些簡單的網頁,通過案例來深刻理解上面的那些原理。

          1. memory cache & disk cache

          我們寫一個簡單的 index.html,然后引用 3 種資源,分別是 index.js, index.css 和 mashroom.jpg。

          我們給這三種資源都設置上 Cache-control: max-age=86400,表示強制緩存 24 小時。以下截圖全部使用 Chrome 的隱身模式。

          1、首次請求

          毫無意外的全部走網絡請求,因為什么緩存都還沒有。

          2、再次請求 (F5)

          第二次請求,三個請求都來自 memory cache。因為我們沒有關閉 TAB,所以瀏覽器把緩存的應用加到了 memory cache。(耗時 0ms,也就是 1ms 以內)

          3、關閉 TAB,打開新 TAB 并再次請求

          因為關閉了 TAB,memory cache 也隨之清空。但是 disk cache 是持久的,于是所有資源來自 disk cache。(大約耗時 3ms,因為文件有點小)

          2. no-cache & no-store

          我們在 index.html 里面一些代碼,完成兩個目標:

          • 每種資源都(同步)請求兩次
          • 增加腳本異步請求圖片

          1、當把服務器響應設置為 Cache-Control: no-cache 時,我們發現打開頁面之后,三種資源都只被請求 1 次。

          這說明兩個問題:

          • 同步請求方面,瀏覽器會自動把當次 HTML 中的資源存入到緩存 (memory cache),這樣碰到相同 src 的圖片就會自動讀取緩存(但不會在 Network 中顯示出來)
          • 異步請求方面,瀏覽器同樣是不發請求而直接讀取緩存返回。但同樣不會在 Network 中顯示。

          總體來說,如上面原理所述,no-cache 從語義上表示下次請求不要直接使用緩存而需要比對,并不對本次請求進行限制。因此瀏覽器在處理當前頁面時,可以放心使用緩存。

          2、當把服務器響應設置為 Cache-Control: no-store 時,情況發生了變化,三種資源都被請求了 2 次。而圖片因為還多一次異步請求,總計 3 次。(紅框中的都是那一次異步請求)

          這同樣說明:

          • 如之前原理所述,雖然 memory cache 是無視 HTTP 頭信息的,但是 no-store 是特別的。在這個設置下,memory cache 也不得不每次都請求資源。
          • 異步請求和同步遵循相同的規則,在 no-store 情況下,依然是每次都發送請求,不進行任何緩存。

          3、Service Worker & memory (disk) cache

          我們嘗試把 Service Worker 也加入進去。我們編寫一個 serviceWorker.js,并編寫如下內容:(主要是預緩存 3 個資源,并在實際請求時匹配緩存并返回)

          注冊 SW 的代碼這里就不贅述了。此外我們還給服務器設置 Cache-Control: max-age=86400 來開啟 disk cache。我們的目的是看看兩者的優先級。

          1、當我們首次訪問時,會看到常規請求之外,瀏覽器(確切地說是 Service Worker)額外發出了 3 個請求。這來自預緩存的代碼。

          2、第二次訪問(無論關閉 TAB 重新打開,還是直接按 F5 刷新)都能看到所有的請求標記為 from SerciceWorker。

          from ServiceWorker 只表示請求通過了 Service Worker,至于到底是命中了緩存,還是繼續 fetch() 方法光看這一條記錄其實無從知曉。因此我們還得配合后續的 Network 記錄來看。因為之后沒有額外的請求了,因此判定是命中了緩存。

          從服務器的日志也能很明顯地看到,3 個資源都沒有被重新請求,即命中了 Service Worker 內部的緩存。

          3、如果修改 serviceWorker.js 的 fetch 事件監聽代碼,改為如下:

          // 這個也叫做 NetworkOnly 的緩存策略。
          self.addEventListener('fetch', e => {
           return e.respondWith(fetch(e.request))
          })
          

          可以發現在后續訪問時的效果和修改前是 完全一致的。(即 Network 僅有標記為 from ServiceWorker 的幾個請求,而服務器也不打印 3 個資源的訪問日志)

          很明顯 Service Worker 這層并沒有去讀取自己的緩存,而是直接使用 fetch() 進行請求。所以此時其實是 Cache-Control: max-age=86400 的設置起了作用,也就是 memory/disk cache。但具體是 memory 還是 disk 這個只有瀏覽器自己知道了,因為它并沒有顯式的告訴我們。(個人猜測是 memory,因為不論從耗時 0ms 還是從不關閉 TAB 來看,都更像是 memory cache)

          瀏覽器的行為

          所謂瀏覽器的行為,指的就是用戶在瀏覽器如何操作時,會觸發怎樣的緩存策略。主要有 3 種:

          • 打開網頁,地址欄輸入地址: 查找 disk cache 中是否有匹配。如有則使用;如沒有則發送網絡請求。
          • 普通刷新 (F5):因為 TAB 并沒有關閉,因此 memory cache 是可用的,會被優先使用(如果匹配的話)。其次才是 disk cache。
          • 強制刷新 (Ctrl + F5):瀏覽器不使用緩存,因此發送的請求頭部均帶有 Cache-control: no-cache(為了兼容,還帶了 Pragma: no-cache)。服務器直接返回 200 和最新內容。

          緩存的應用模式

          了解了緩存的原理,我們可能更加關心如何在實際項目中使用它們,才能更好的讓用戶縮短加載時間,節約流量等。這里有幾個常用的模式,供大家參考

          模式 1:不常變化的資源

          Cache-Control: max-age=31536000
          

          通常在處理這類資源資源時,給它們的 Cache-Control 配置一個很大的 max-age=31536000 (一年),這樣瀏覽器之后請求相同的 URL 會命中強制緩存。而為了解決更新的問題,就需要在文件名(或者路徑)中添加 hash, 版本號等動態字符,之后更改動態字符,達到更改引用 URL 的目的,從而讓之前的強制緩存失效 (其實并未立即失效,只是不再使用了而已)。

          在線提供的類庫 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用這個模式。如果配置中還增加 public 的話,CDN 也可以緩存起來,效果拔群。

          這個模式的一個變體是在引用 URL 后面添加參數 (例如 ?v=xxx 或者 ?_=xxx),這樣就不必在文件名或者路徑中包含動態參數,滿足某些完美主義者的喜好。在項目每次構建時,更新額外的參數 (例如設置為構建時的當前時間),則能保證每次構建后總能讓瀏覽器請求最新的內容。

          特別注意: 在處理 Service Worker 時,對待 sw-register.js(注冊 Service Worker) 和 serviceWorker.js (Service Worker 本身) 需要格外的謹慎。如果這兩個文件也使用這種模式,你必須多多考慮日后可能的更新及對策。

          模式 2:經常變化的資源

          Cache-Control: no-cache
          

          這里的資源不單單指靜態資源,也可能是網頁資源,例如博客文章。這類資源的特點是:URL 不能變化,但內容可以(且經常)變化。我們可以設置 Cache-Control: no-cache 來迫使瀏覽器每次請求都必須找服務器驗證資源是否有效。

          既然提到了驗證,就必須 ETag 或者 Last-Modified 出場。這些字段都會由專門處理靜態資源的常用類庫(例如 koa-static)自動添加,無需開發者過多關心。

          也正如上文中提到協商緩存那樣,這種模式下,節省的并不是請求數,而是請求體的大小。所以它的優化效果不如模式 1 來的顯著。

          模式 3:非常危險的模式 1 和 2 的結合 (反例)

          Cache-Control: max-age=600, must-revalidate
          

          不知道是否有開發者從模式 1 和 2 獲得一些啟發:模式 2 中,設置了 no-cache,相當于 max-age=0, must-revalidate。我的應用時效性沒有那么強,但又不想做過于長久的強制緩存,我能不能配置例如 max-age=600, must-revalidate 這樣折中的設置呢?

          表面上看這很美好:資源可以緩存 10 分鐘,10 分鐘內讀取緩存,10 分鐘后和服務器進行一次驗證,集兩種模式之大成,但實際線上暗存風險。因為上面提過,瀏覽器的緩存有自動清理機制,開發者并不能控制。

          舉個例子:當我們有 3 種資源: index.html, index.js, index.css。我們對這 3 者進行上述配置之后,假設在某次訪問時,index.js 已經被緩存清理而不存在,但 index.html, index.css 仍然存在于緩存中。這時候瀏覽器會向服務器請求新的 index.js,然后配上老的 index.html, index.css 展現給用戶。這其中的風險顯而易見:不同版本的資源組合在一起,報錯是極有可能的結局。

          除了自動清理引發問題,不同資源的請求時間不同也能導致問題。例如 A 頁面請求的是 A.js 和 all.css,而 B 頁面是 B.js 和 all.css。如果我們以 A -> B 的順序訪問頁面,勢必導致 all.css 的緩存時間早于 B.js。那么以后訪問 B 頁面就同樣存在資源版本失配的隱患。

          有開發者朋友(wd2010)在知乎的評論區提了一個很好的問題:

          如果我不使用must-revalidate,只是Cache-Control: max-age=600,瀏覽器緩存的自動清理機制就不會執行么?如果瀏覽器緩存的自動清理機制執行的話那后續的index.js被清掉的所引發的情況都是一樣的呀!

          這個問題涉及幾個小點,需要補充說明一下:

          1、'max-age=600' 和 'max-age=600,must-revalidate' 有什么區別?

          沒有區別。在列出 max-age 了之后,must-revalidate 是否列出效果相同,瀏覽器都會在超過 max-age 之后進行校驗,驗證緩存是否可用。

          在 HTTP 的規范中,只闡述了 must-revalidate 的作用,卻沒有闡述不列出 must-revalidate 時,瀏覽器應該如何解決緩存過期的問題,因此這其實是瀏覽器實現時的自主決策。(可能有少數瀏覽器選擇在源站點無法訪問時繼續使用過期緩存,但這取決于瀏覽器自身)

          2、那 'max-age=600' 是不是也會引發問題?

          是的。問題的出現和是否列出 'must-revalidate' 無關,依然會存在 JS CSS等文件版本失配的問題。因此常規的網站在不同頁面需要使用不同的 JS CSS 文件時,如果要使用 max-age 做強緩存,不要設置一個太短的時間。

          3、那這類比較短的 max-age 到底能用在哪里呢?

          既然版本存在失配的問題,那么要避開這個問題,就有兩種方法。

          a、整站都使用相同的 JS 和 CSS,即合并后的文件。這個比較適合小型站點,否則可能過于冗余,影響性能。(不過可能還是會因為瀏覽器自身的清理策略被清理,依然有隱患)

          b、資源是獨立使用的,并不需要和其他文件配合生效。例如 RSS 就歸在此類。

          總結

          這篇文章有點,不過如果大伙如果能靜下心來看完并理解,相信你對前端緩存一定能更上一層樓。


          eb存儲機制

          storage類型介紹

          storage類型是來存儲名值對兒,是保存在瀏覽器端的,存儲為key-value形式的。

          1. key:作為存儲數據的標識(唯一且不可重復)
          2. value:存儲數據的內容(number/string)

          該類型的實例對象有關于存儲的方法如下:

          (1) clear():刪除所有值。

          (2) getItem():根據指定的名字(key)獲取對應的值。

          (3) key(index):獲取index位置處的值的名字(key)。

          (4) removeItem():刪除由name指定的名值對兒。

          (5) setItem(name,value): 為指定的name 設置一個對應的值。

          (6) Storage.length :返回一個整數,表示存儲在 Storage 對象中的數據項數量。這個是 Storage 對象的一個屬性,而且是一個 只讀 屬性

          我自己是一名從事了多年開發的web前端老程序員,目前辭職在做自己的web前端私人定制課程,今年我花了一個月整理了一份最適合2020年學習的web前端學習干貨,各種框架都有整理,送給每一位前端小伙伴,想要獲取的可以關注我的頭條號并在后臺私信我:前端,即可免費獲取。

          storage特性

          該類型只能存儲字符串,非字符串的數據會在存儲之前轉換成字符串。

          sessionStorage對象

          sessionStorage對象存儲是web stroage機制的一種,該對象會給每一個給定的源維持一個獨立的存儲區域,該區域在頁面會話期間可用(即只要瀏覽器處于打開狀態,包括頁面重新加載和恢復) sessionStorage對象是綁定在服務器會話的,所以當文件在本地運行的時候是不能跨頁面訪問的,使用服務器地址訪問時才能多頁面訪問。


          在另一個頁面打開是找不到這個值的:


          開啟服務器訪問:



          再次使用服務器地址訪問的時候:



          localStorage對象

          localStorage也是web Stroage存儲機制的一種,localStorage對象是根據glocalStorage對象設計出來的簡化,glocalStorage的使用是要指定哪些域名可以訪問該數據,是通過方括號標記使用該屬性來實現的:

          //存數據
          glocalStorage["baidu.com"].name="劉德華"
          //取數據
          var name=glocalStorage["baidu.com"].name
          

          聽網上說是為了安全性然后在HTML5的規范中用localStorage取代了glocalStorage不能自己設置域名,值使用localStorage對象時就將當前域名加入。規則設計出來就是方便我們的使用的,顯然localStorage比之前的使用起來更簡單了。

          我自己的理解根據該用戶不刪除數據,數據將會一直保存在磁盤里的屬性,可以設置收藏列表,用戶愛好等等。

          //存數據
          glocalStorage.setItem('content', '劉德華');
          //取數據
          glocalStorage.getItem('content');

          兩者的比較

          不同點

          數據存儲時長

          sessionStorage:數據只保持在瀏覽器關閉

          localStorage:用戶不刪除數據,數據將會一直保存在磁盤里

          訪問規則不同

          localStorage:只要在相同的協議、相同的主機名、相同的端口下,就能讀取和修改到同一份 localStorage 存儲的數據。

          sessionStorage:除了協議、主機名、端口外,還要求在同一窗口下。

          相同點

          1. 存儲大小都不能超過5M。

          Http緩存

          Last-Modified與if-Modified-Since

          Last-Modified:響應頭,資源最新修改時間,由服務器告訴瀏覽器。 if-Modified-Since:請求頭,資源最新修改時間,由瀏覽器告訴服務器(其實就是上次服務器給的Last-Modified,請求又還給服務器對比),和Last-Modified是一對,它兩會在服務器端進行對比。

          請求過程

          第一次請求數據

          瀏覽器:服務器服務器,我現在需要一個users.json的文件,你找到了給我,順便給我文件修改時間!

          服務器:行,文件修改時間我給你,到時候文件過期了咱兩核對文件修改時間,對得上我就不找了,返回a.txt+Last-Modified。

          第二次請求數據 瀏覽器:服務器服務器,我還需要一個users.json的文件,我把這個文件上次修改時間發你if-Modified-Since,你對比一下文件最近有不有修改!

          服務器:ok,我幫你找一下。服務器將if-Modified-Since與Last-Modified做了個對比。

          if-Modified-Since 與Last-Modified不相等,服務器查找了最新的users.json,同時再次返回全新的Last-Modified。

          if-Modified-Since 與Last-Modified相等,服務器返回狀態碼304,文件沒修改過,你還是用你的本地緩存。

          Etag與If-None-Match

          請求過程

          Etag:響應頭,由服務器設置告訴瀏覽器。

          if-None-Match:請求頭,由瀏覽器告訴服務器(其實就是上次服務器給的Etag),和Etag是一對,它兩也會在服務器端進行對比。

          瀏覽器:服務器服務器,我現在需要一個users.json的文件,你找到了給我!

          服務器:你又來了,文件我找給你,我給你一個Etag(文件版本號),到時候你再來找這個資源的時候把這個帶上,省的我再找給你,返回a.txt+Etag。

          第二次請求數據 瀏覽器:服務器服務器,我又需要users.json文件了,我把你發給我的Etag放在If-None-Match里面了,你對比一下最近這個文件有不有修改!

          服務器:ok,我幫你找一下。服務器將Etag與If-None-Match做了個對比。 Etag與If-None-Match不相等,服務器查找了最新的users.json,同時再次返回全新的Etag(文件版本號)。

          Etag與If-None-Match相等,服務器返回狀態碼304,文件沒修改過,你還是用你的本地緩存。

          Http頭部緩存和web緩存例子

          模擬前端請求顯示數據

           <ul id="users"></ul>
              <script>
                  window.onload = function() {
                      const users = JSON.parse(localStorage.getItem('usersdata'));
                      console.log("users", users)
                      const uluser = document.querySelector('#users');
                      if (users) {
                          uluser.innerHTML = users.map(user =>
                                  `
                              <li>
                                  ${user.username}-${user.place}
                              </li>
                              `
                              )
                      }
                      fetch('/users')
                          .then(data => data.json())
                          .then(data => {
                              localStorage.setItem('usersdata', JSON.stringify(data)) //需要存儲字符串
                              console.log("users請求")
                              uluser.innerHTML = data.map(user =>
                                  `
                                  <li>
                                      ${user.username}-${user.place}
                                  </li>
                                  `
                              )
                          })
                  }
              </script>

          模擬服務器接收與發送

          const http = require('http');
          const fs = require('fs');
          const users = require('./users.json')
          http.createServer((req, res) => {
              let status = fs.statSync('users.json');//獲取文件的狀態
              if (req.url == '/') {
                  res.writeHead(200, { "Content-Type": "text/html;charset=utf8" })
                   fs.createReadStream('./users.html')pipe(res);//通過流返回顯示頁面
              } else if (req.url == '/users') {
                  console.log("用戶數據請求")
                  if (req.headers['if-modified-since']) {
                      if (req.headers['if-modified-since'] == status.mtime) {//判斷請求中的修改時間與文件的修改時間是否相同
                          console.log("未修改文件返回304")
                          res.writeHead(304, { "Content-Type": "text/html;charset=utf8" });//304 Not Modified
                          res.end();
                          return;
                      } else {
                          res.setHeader('Last-Modified', status.mtime);
                          res.writeHead(200, { "Content-Type": "text/html;charset=utf8" });
                          console.log('修改了返回修改數據')
                          res.end(JSON.stringify(users))
                          return;
                      }
                  }
                  console.log("用戶數據第一次")
                  res.setHeader('Last-Modified', status.mtime);
                  res.writeHead(200, { "Content-Type": "text/html;charset=utf8" });
                  res.end(JSON.stringify(users))
              } else {
                  res.end("null")
              }
          }).listen('8088', () => {
              console.log('監聽8088')
          })

          結果分析

          第一次請求數據服務器給我們返回200狀態碼和Last-Modified,代碼中提示的地方提示出用戶為第一次數據請求。



          文件未修改,第二次請求數據服務端判斷出文件未修改,則返回304狀態碼官方解釋304:The HTTP 304 Not Modified client redirection response code indicates that there is no need to retransmit the requested resources.



          文件修改,再次訪問,服務端檢測文件修改時間不一樣,則重新返回文件內容




          結尾

          緩存數據可以節省許多資源,對于以上例子還有些缺陷,對于高頻訪問情況還是不能滿足需求,技術就是需要不斷的探索。有什么建議或者錯誤,歡迎指出!


          作者:有酒有故事
          鏈接:https://juejin.im/post/6844904164540022797


          主站蜘蛛池模板: 久久国产精品最新一区| 东京热无码一区二区三区av| 国产一区二区三区免费看| 一区二区三区伦理高清| 亚洲狠狠久久综合一区77777 | 国产精品电影一区| 国产在线一区二区三区在线| 色窝窝无码一区二区三区 | 国产成人一区二区三区在线观看| 无码人妻一区二区三区一| 美女福利视频一区| 国产成人高清精品一区二区三区| 亚洲综合在线一区二区三区| 精品福利视频一区二区三区| 一夲道无码人妻精品一区二区| 国产福利电影一区二区三区久久久久成人精品综合 | 久久精品无码一区二区三区| 国产午夜福利精品一区二区三区| 中文字幕无线码一区二区 | 日韩一区二区三区精品| 国产一区二区三区乱码网站| 精品一区二区久久久久久久网站| 在线电影一区二区三区| 亚洲AV日韩AV天堂一区二区三区| 国产伦精品一区二区三区免.费| 中文字幕一区二区三区精彩视频| 国产亚洲无线码一区二区| 国产精品视频一区国模私拍| 亚洲sm另类一区二区三区| 亚洲AV无码一区二区三区在线| 亚洲一区二区三区高清| 精品无码av一区二区三区| 精品无码人妻一区二区三区 | 精品国产一区二区三区久久久狼 | 日本人的色道www免费一区| 精品人妻码一区二区三区 | 精品人伦一区二区三区潘金莲| 无码喷水一区二区浪潮AV| 免费一区二区无码视频在线播放| 国产主播一区二区三区在线观看 | 色狠狠AV一区二区三区|