爲何要使用web緩存?
Web緩存存在於服務器和客戶端之間。Web緩存密切注視着服務器-客戶端之間的通訊,監控請求,而且把請求輸出的內容(例如html頁面、 圖片和文件)另存一份;而後,若是下一個請求是相同的URL,則直接使用保存的副本,而不是再次請求源服務器。php
使用Web緩存的好處是顯而易見的:css
如今的大型網站,隨便一個頁面都是一兩百個請求,天天 pv 都是億級別,若是沒有緩存,用戶體驗會急劇降低、同時服務器壓力和網絡帶寬都面臨嚴重的考驗。 緩存和重用之前獲取的資源的是優化網頁性能很重要的一個方面。前端
缺點也是有的:html5
- 緩存沒有清理機制--這些緩存的文件會永久性地保存在機器上,在特定的時間內,這些文件多是幫了你大忙,可是時間一長,咱們已經再也不須要瀏覽以前的這些網頁,這些文件就成了無效或者無用的文件,它們存儲在用戶硬盤中只會佔用空間而沒有任何用處,若是要緩存的東西很是多,那就會撐暴整個硬盤空間。
- 給開發帶來的困擾--明明修改了樣式文件、圖片、視頻或腳本,刷新頁面或部署到站點以後看不到修改以後的效果。
因此在產品開發的時候咱們老是想辦法避免緩存產生,而在產品發佈之時又在想策略管理緩存提高網頁的訪問速度。瞭解瀏覽器的緩存命中原理和清除方法,對咱們大有裨益。java
緩存的分類
在Web應用領域,Web緩存大體能夠分爲如下幾種類型:nginx
1.數據庫數據緩存
Web應用,特別是社交網絡服務類型的應用,每每關係比較複雜,數據庫表繁多,若是頻繁進行數據庫查詢,很容易致使數據庫不堪重荷。爲了提供查詢的性能,會將查詢後的數據放到內存中進行緩存,下次查詢時,直接從內存緩存直接返回,提供響應效率。好比經常使用的緩存方案有memcached,redis等。 web
2.服務器端緩存
代理服務器緩存
代理服務器是瀏覽器和源服務器之間的中間服務器,瀏覽器先向這個中間服務器發起Web請求,通過處理後(好比權限驗證,緩存匹配等),再將請求轉發到源服務器。代理服務器緩存的運做原理跟瀏覽器的運做原理差很少,只是規模更大。能夠把它理解爲一個共享緩存,不僅爲一個用戶服務,通常爲大量用戶提供服務,所以在減小相應時間和帶寬使用方面頗有效,同一個副本會被重用屢次。常見代理服務器緩存解決方案有Squid,Nginx,Apache等。ajax
CDN緩存
CDN(Content delivery networks)緩存,也叫網關緩存、反向代理緩存。CDN緩存通常是由網站管理員本身部署,爲了讓他們的網站更容易擴展並得到更好的性能。瀏覽器先向CDN網關發起Web請求,網關服務器後面對應着一臺或多臺負載均衡源服務器,會根據它們的負載請求,動態將請求轉發到合適的源服務器上。雖然這種架構負載均衡源服務器之間的緩存無法共享,但卻擁有更好的處擴展性。從瀏覽器角度來看,整個CDN就是一個源服務器,瀏覽器和服務器之間的緩存機制,在這種架構下一樣適用。redis
3.瀏覽器端緩存
瀏覽器緩存根據一套與服務器約定的規則進行工做,在同一個會話過程當中會檢查一次並肯定緩存的副本足夠新。若是你瀏覽過程當中,好比前進或後退,訪問到同一個圖片,這些圖片能夠從瀏覽器緩存中調出而即時顯現。
4.Web應用層緩存
應用層緩存指的是從代碼層面上,經過代碼邏輯和緩存策略,實現對數據,頁面,圖片等資源的緩存,能夠根據實際狀況選擇將數據存在文件系統或者內存中,減小數據庫查詢或者讀寫瓶頸,提升響應效率。
緩存如何發揮做用
請看下面的圖:
緩存控制設置字段和原理
1.HTML Meta標籤控制緩存
瀏覽器緩存機制,其實主要就是HTTP協議定義的緩存機制(如: Expires; Cache-control等)。可是也有非HTTP協議定義的緩存機制,如使用HTML Meta 標籤,Web開發者能夠在HTML頁面的<head>節點中加入<meta>標籤,代碼以下:
<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
<META HTTP-EQUIV="Expires" CONTENT="0">
上述代碼的做用是告訴瀏覽器當前頁面不被緩存,每次訪問都須要去服務器拉取。使用上很簡單,但只有部分瀏覽器能夠支持,
事實上這種禁用緩存的形式用處頗有限:
1. 僅有IE才能識別這段meta標籤含義,其它主流瀏覽器僅能識別「Cache-Control: no-store」的meta標籤。
2. 在IE中識別到該meta標籤含義,並不必定會在請求字段加上Pragma,但的確會讓當前頁面每次都發新請求(僅限頁面,頁面上的資源則不受影響)。
並且全部緩存代理服務器都不支持,由於代理不解析HTML內容自己。而普遍應用的仍是 HTTP頭信息來控制緩存,下面我主要介紹HTTP協議定義的緩存機制
2.HTTP頭信息控制緩存
咱們先來瞅一眼http1.1協議報文首部字段中與緩存相關的字段
1.通用首部字段
2.請求首部字段
3.響應首部字段
4.實體首部字段
http1.0 時代緩存字段詳解
在 http1.0 時代,給客戶端設定緩存方式可經過兩個字段Pragma
和Expires
來規範。雖然這兩個字段早可拋棄,但http協議作了向下兼容,因此依然能夠看到。
1.Pragma
Pragma:設置頁面是否緩存,爲Pragma則緩存,no-cache則不緩存
當該字段值爲no-cache
的時候,會知會客戶端不要對該資源讀緩存,即每次都得向服務器發一次請求才行。
2.Expires
有了Pragma來禁用緩存,天然也須要有個東西來啓用緩存和定義緩存時間,對http1.0而言,Expires就是作這件事的首部字段。 Expires的值對應一個GMT(格林尼治時間),好比Mon, 22 Jul 2002 11:12:01 GMT
來告訴瀏覽器資源緩存過時時間,若是還沒過該時間點則不發請求。
若是Pragma頭部和Expires頭部同時存在,則起做用的會是Pragma,須要注意的是,響應報文中Expires所定義的緩存時間是相對服務器上的時間而言的,其定義的是資源「失效時刻」,若是客戶端上的時間跟服務器上的時間不一致(特別是用戶修改了本身電腦的系統時間),那緩存時間可能就沒啥意義了。
http1.1時代緩存字段詳解
1.Cache-Control
針對上述的「Expires時間是相對服務器而言,沒法保證和客戶端時間統一」的問題,http1.1新增了 Cache-Control 來定義緩存過時時間。注意:若報文中同時出現了 Expires 和 Cache-Control,則以 Cache-Control 爲準。
也就是說優先級從高到低分別是 Pragma -> Cache-Control -> Expires 。
Cache-Control也是一個通用首部字段,這意味着它能分別在請求報文和響應報文中使用。在RFC中規範了 Cache-Control 的格式爲:
"Cache-Control" ":" cache-directive
做爲請求首部時,cache-directive 的可選值有:
Cache-Control: no-cache:這個很容易讓人產生誤解,令人誤覺得是響應不被緩存。實際上Cache-Control: no-cache是會被緩存的,
只不過每次在向客戶端(瀏覽器)提供響應數據時,緩存都要向服務器評估緩存響應的有效性。
Cache-Control: no-store:這個纔是響應不被緩存的意思。
做爲響應首部時,cache-directive 的可選值有:
Cache-Control 容許自由組合可選值,例如:
Cache-Control: max-age=3600, must-revalidate
它意味着該資源是從原服務器上取得的,且其緩存(新鮮度)的有效時間爲一小時,在後續一小時內,用戶從新訪問該資源則無須發送請求。
固然這種組合的方式也會有些限制,好比 no-cache 就不能和 max-age、min-fresh、max-stale 一塊兒搭配使用。
2.Last-Modified/If-Modified-Since
Last-Modified/If-Modified-Since要配合Cache-Control使用。
(1) Last-Modified:標示這個響應資源的最後修改時間。web服務器在響應請求時,告訴瀏覽器資源的最後修改時間。
(2) If-Modified-Since:當資源過時時(使用Cache-Control標識的max-age),發現資源具備Last-Modified聲明,則再次向web服務器請求時帶上頭 If-Modified-Since,表示請求時間。web服務器收到請求後發現有頭If-Modified-Since 則與被請求資源的最後修改時間進行比對。若最後修改時間較新,說明資源又被改動過,則響應整片資源內容(寫在響應消息包體內),HTTP 200;若最後修改時間較舊,說明資源無新修改,則響應HTTP 304 (無需包體,節省瀏覽),告知瀏覽器繼續使用所保存的cache。
3.Etag/If-None-Match
Etag/If-None-Match也要配合Cache-Control使用。
(1) Etag:web服務器響應請求時,告訴瀏覽器當前資源在服務器的惟一標識(生成規則由服務器以爲)。Apache中,ETag的值,默認是對文件的索引節(INode),大小(Size)和最後修改時間(MTime)進行Hash後獲得的。
(2)If-None-Match:當資源過時時(使用Cache-Control標識的max-age),發現資源具備Etage聲明,則再次向web服務器請求時帶上頭If-None-Match (Etag的值)。web服務器收到請求後發現有頭If-None-Match 則與被請求資源的相應校驗串進行比對,決定返回200或304。
4.既生Last-Modified何生Etag?
你可能會以爲使用Last-Modified已經足以讓瀏覽器知道本地的緩存副本是否足夠新,爲何還須要Etag(實體標識)呢?HTTP1.1中Etag的出現主要是爲了解決幾個Last-Modified比較難解決的問題:
(1) Last-Modified標註的最後修改只能精確到秒級,若是某些文件在1秒鐘之內,被修改屢次的話,它將不能準確標註文件的修改時間
(2)若是某些文件會被按期生成,當有時內容並無任何變化,但Last-Modified卻改變了,致使文件無法使用緩存
(3)有可能存在服務器沒有準確獲取文件修改時間,或者與代理服務器時間不一致等情形
Etag是服務器自動生成或者由開發者生成的對應資源在服務器端的惟一標識符,可以更加準確的控制緩存。Last-Modified與ETag是能夠一塊兒使用的,服務器會優先驗證ETag,一致的狀況下,纔會繼續比對Last-Modified,最後才決定是否返回304。
5.不太經常使用的兩個http字段If-Unmodified-Since/If-Match
(1)If-Unmodified-Since: Last-Modified-value
告訴服務器,若Last-Modified沒有匹配上(資源在服務端的最後更新時間改變了),則應當返回412(Precondition Failed) 狀態碼給客戶端。
當遇到下面狀況時,If-Unmodified-Since 字段會被忽略:
1. Last-Modified值對上了(資源在服務端沒有新的修改);
2. 服務端需返回2XX和412以外的狀態碼;
3. 傳來的指定日期不合法
(2)If-Match: ETag-value
告訴服務器若是沒有匹配到ETag,或者收到了「*」值而當前並無該資源實體,則應當返回412(Precondition Failed) 狀態碼給客戶端。不然服務器直接忽略該字段。
瀏覽器緩存流程圖
小結一下,瀏覽器第一次請求
瀏覽器第二次請求
如何配置
1)經過代碼的方式,在web服務器返回的響應中添加Expires和Cache-Control Header;
好比在JavaWeb裏面,咱們可使用相似下面的代碼設置強緩存:
java.util.Date date = new java.util.Date();
response.setDateHeader("Expires",date.getTime()+20000); //Expires:過期期限值
response.setHeader("Cache-Control", "public"); //Cache-Control來控制頁面的緩存與否,public:瀏覽器和緩存服務器均可以緩存頁面信息;
response.setHeader("Pragma", "Pragma"); //Pragma:設置頁面是否緩存,爲Pragma則緩存,no-cache則不緩存
還能夠經過相似下面的java代碼設置不啓用強緩存:
response.setHeader( "Pragma", "no-cache" );
response.setDateHeader("Expires", 0);
response.addHeader( "Cache-Control", "no-cache" );//瀏覽器和緩存服務器都不該該緩存頁面信息
2)經過配置web服務器的方式,讓web服務器在響應資源的時候統一添加Expires和Cache-Control Header。
tomcat提供了一個ExpiresFilter專門用來配置強緩存
nginx和apache做爲專業的web服務器,都有專門的配置文件,能夠配置expires和cache-control,
Nginx服務器的配置方法爲:
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
#過時時間爲30天,#圖片文件不怎麼更新,過時能夠設大一點,
expires 30d;
}
location ~ .*\.(js|css)$ {
#若是頻繁更新,則能夠設置得小一點。
expires 1d;
add_header Cache-Control max-age=86400;
etag on;
}
Apache服務器的配置方法爲:
<Location ~ "\.(js|css|png|jpg|gif|bmp|html)$">
ExpiresActive On
ExpiresDefault "access plus 1 hours"
Header set Cache-Control max-age=3600
Header unset Pragma
</Location>
<Location ~ "\.(do|jsp|aspx|asp|php|json|action|ashx|axd|cgi)$">
Header set Cache-Control no-cache,no-store,max-age=0
Header unset Expires
Etag INode Mtime Size
</Location>
3.緩存配置的一些注意事項
1.只有get請求會被緩存,post請求不會
2.Etag 在資源分佈在多臺機器上時,對於同一個資源,不一樣服務器生成的Etag可能不相同,此時就會致使304協議緩存失效,客戶端仍是直接從server取資源。能夠本身修改服務器端etag的生成方式,根據資源內容生成一樣的etag。須要注意的是分佈式系統裏多臺機器間文件的last-modified必須保持一致,以避免負載均衡到不一樣機器致使比對失敗,Yahoo建議分佈式系統儘可能關閉掉Etag(每臺機器生成的etag都會不同,由於除了 last-modified、文檔節點也很難保持一致)
用戶行爲與緩存
緩存的清除方法
因爲在開發的時候不會專門去配置強緩存,而瀏覽器又默認會緩存圖片,css和js等靜態資源,因此開發環境下常常會由於強緩存致使資源沒有及時更新而看不到最新的效果,解決這個問題的方法有不少,經常使用的有如下幾種:
1)直接ctrl+f5,這個辦法能解決頁面直接引用的資源更新的問題;
2)使用ctrl+shift+delete清除緩存;
3)若是用的是chrome,能夠F12在network那裏把緩存給禁掉(這是個很是有效的方法):
4)在開發階段,給資源加上一個動態的參數,如css/index.css?v=0.0001,因爲每次資源的修改都要更新引用的位置,同時修改參數的值,因此操做起來不是很方便,通常使用前端的構建工具來修改這個參數或 在動態頁面好比jsp裏開發就能夠用服務器變量來解決(v=${sysRnd});
1.原生寫法
function addVersion(asset){
asset.forEach(function(item,index){
if(item.indexOf('.js') != -1){
document.write('<script src="'+item+'?v='+ (new Date().getTime()) +'"><\/script>');
}else if(item.indexOf('.css') != -1){
document.write('<link rel="stylesheet" href="'+item+'?v='+(new Date().getTime())+'">');
}
});
}
2.採用gulp插件
(1)gulp-rev-append
(2)gulp-rev和gulp-rev-collector也能實現一樣的功能
// 修改html和css文件,給靜態文件打戳
gulp.task('stamp', function(){
gulp.src(['rev/*.json', dest.css + "**/*.css"]).
pipe(revCollector({
replaceReved: true
})).
// 修改成 ?v=stamp 形式
pipe(replace(/\-([0-9a-z]{8,})\.(png|jpg|gif|ico)/g, function(a, b, c){
return '.' + c + '?v=' + b;
})).
pipe(gulp.dest(dest.css));
gulp.src(['rev/*.json', src.html]).
pipe(revCollector({
replaceReved: true
})).
// 修改成 ?v=stamp 形式
pipe(replace(/\-([0-9a-z\-]{8,})\.(css|js)/g, function(a, b, c){
return '.' + c + '?v=' + b;
})).
pipe(gulp.dest(dest.html));
});
5)若是資源引用的頁面,被嵌入到了一個iframe裏面,能夠在iframe的區域右鍵單擊從新加載該頁面,以chrome爲例:
6)若是緩存問題出如今ajax請求中,最有效的解決辦法就是ajax的請求地址追加隨機數;
7)還有一種狀況就是動態設置iframe的src時,有可能也會由於緩存問題,致使看不到最新的效果,這時候在要設置的src後面添加隨機數也能解決問題;
8)若是你用的是grunt和gulp這種前端工具開發,經過它們的插件好比grunt-contrib-connect或gulp-connect來啓動一個靜態服務器,則徹底不用擔憂開發階段的資源更新問題,由於在這個靜態服務器下的全部資源返回的respone header中,cache-control始終被設置爲不緩存:
與之相關的--本地存儲和離線存儲
localStorage/sessionStorage
localStorage.setItem("name", "Robert");
localStorage.getItem("name");
這樣的存取最多能夠存儲5M的數據(localStorge在Android webview中不支持擴容,只有在pc瀏覽器中超限纔會彈出擴容提示 ),給你更多選擇的空間。可是因爲本地存儲是基於字符串的存儲,存儲一串沒有結構的字符串並非一個理想的選擇。所以,咱們能夠利用瀏覽器中原生的JSON支持來將JavaScript對象轉化成字符串,從而保存到本地數據中,在讀取的時候也能夠將其轉換回JavaScript對象。
緩存和使用圖片的方法
//在本地存儲中保存圖片
var storageFiles = JSON.parse(localStorage.getItem("storageFiles")) || {},
elephant = document.getElementById("elephant"),
storageFilesDate = storageFiles.date,
date = new Date(),
todaysDate = (date.getMonth() + 1).toString() + date.getDate().toString();
// 檢查數據,若是不存在或者數據過時,則建立一個本地存儲
if (typeof storageFilesDate === "undefined" || storageFilesDate < todaysDate) {
// 圖片加載完成後執行
elephant.addEventListener("load", function () {
var imgCanvas = document.createElement("canvas"),
imgContext = imgCanvas.getContext("2d");
// 確保canvas尺寸和圖片一致
imgCanvas.width = elephant.width;
imgCanvas.height = elephant.height;
// 在canvas中繪製圖片
imgContext.drawImage(elephant, 0, 0, elephant.width, elephant.height);
// 將圖片保存爲Data URI
storageFiles.elephant = imgCanvas.toDataURL("image/png");
storageFiles.date = todaysDate;
// 將JSON保存到本地存儲中
try {
localStorage.setItem("storageFiles", JSON.stringify(storageFiles));
}
catch (e) {
console.log("Storage failed: " + e);
}
}, false);
// 設置圖片
elephant.setAttribute("src", "elephant.png");
}
else {
// Use image from localStorage
elephant.setAttribute("src", storageFiles.elephant);
}
sessionStorage的數據只存儲到特定的會話中,不屬於持久化的存儲,因此關閉瀏覽器會清除數據。和localstorage具備相同的方法。
離線存儲
設置方法
1. 在HTML5的html標籤中添加一個 manifest="XXX.appcache" 屬性聲明
<!DOCTYPE html>
<html manifest="list.appcache">
2.XXX.appcache文件中定義須要緩存的文件清單(裏面的資源文件的路徑是相對於manifest的路徑而言的)
CACHE MANIFEST
# VERSION 0.3
# 直接緩存的文件
CACHE:
# 須要在線訪問的文件
NETWORK:
# 替代方案
FALLBACK:
CACHE MANIFEST --(必須) 此標題下列出的文件將在首次下載後進行緩存
#V1.0.2
../addDevice.html
../static/css/reset.css
../static/js/addDevice.js
../static/img/ms1.png
../static/img/clean-face.jpg
NETWORK----(可選)
(1)通配符'*'表示不在CACHE MANIFEST清單裏的文件,每次都要從新請求
*
(2)或者指定特定文件,好比login.asp不被離線存儲,每次都要從新發起請求
login.asp
FALLBACK----(可選) 斷網時訪問指定路徑時的替換文件
如斷網時訪問/html5/ 目錄下的全部資源文件,則用 "offline.html" 替代
/html5/ /offline.html
更新原理
更新了manifest文件,瀏覽器會自動的從新下載新的manifest文件並把manifest緩存列表中的全部文件從新請求一次(第二次刷新替換本地緩存爲最新緩存),而不是單獨請求某個特定修改過的資源文件,由於manifest是不知道哪一個文件被修改過了的。
對於全局更新沒必要要擔憂,由於沒有更新過的資源文件,請求依舊是304響應,只有真正更新過的資源文件纔是服務器返回的纔是200.
因此控制離線存儲的更新,須要2個步驟,一是更新資源文件,二是更新manifest文件,只要修改manifest文件隨意一處,瀏覽器就會感知manifest文件更新,而咱們的資源文件名稱一般是固定的,須要更新manifest文件怎麼操做呢?一個比較好的方式是更新以# 開頭的版本號註釋,告訴瀏覽器這個manifest文件被更新過。
manifest資源是滯後靜默更新的
第二次刷新界面以後,才能看到更新後的效果
/*code1,簡單粗暴的*/
applicationCache.onupdateready = function(){ applicationCache.swapCache(); //強制替換緩存 location.reload(); //從新加載頁面 }; /*code2,緩存公用方法*/ // var EventUtil = { // addHandler: function(element, type, handler) { // if (element.addEventListener) { // element.addEventListener(type, handler, false); // } else if (element.attachEvent) { // element.attachEvent("on" + type, handler); // } else { // element["on" + type] = handler; // } // } // }; // EventUtil.addHandler(applicationCache, "updateready", function() { //緩存更新並已下載,要在下次進入頁面生效 // applicationCache.update(); //檢查緩存manifest文件是否更新,ps:頁面加載默認檢查一次。 // applicationCache.swapCache(); //交換到新的緩存項中,交換了要下次進入頁面才生效 // location.reload(); //從新載入頁面 // });
applicationCache 提供了以下的事件:
Event handler Event handler event type
onchecking checking
onerror error
onnoupdate noupdate
ondownloading downloading
onprogress progress
onupdateready updateready
oncached cached
onobsolete obsolete
提供了以下的API:
void update();
// 更新, 可是這個方法適用於一些長期打開的頁面,而不會有刷新動做,好比郵件系統,因此這個就比較適合作自動更新下載
void abort();
// 取消
void swapCache();
// 替換緩存內容 ,對於manifest文件的改變,一般是下一次的刷新纔會觸發下載更新,第二次刷新纔會切換使用新的緩存文件,經過這個方法,能夠強制將緩存替換
注意事項
站點中的其餘頁面即便沒有設置manifest屬性,請求的資源若是在緩存中也從緩存中訪問
系統會自動緩存引用清單文件的 HTML 文件
若是manifest文件,或者內部列舉的某一個文件不能正常下載,整個更新過程將視爲失敗,瀏覽器繼續所有使用老的緩存
在manifest中使用的相對路徑,相對參照物爲manifest文件
站點離線存儲的容量限制是5M
manifest文件中CACHE則與NETWORK,FALLBACK的位置順序沒有關係,若是是隱式聲明須要在最前面
manifest中必須一一聲明文件名,這很使人頭痛
引用manifest的html必須與manifest文件同源,在同一個域下
除此以外,還增長了兩大問題:
(1)PV UV的計算難題,因爲當前頁面被強制加入manifest,那麼PV 和UV的統計,成了一個難題,由於請求再也不是發送到服務器;
(2)緩存對於某個使用manifest的文件,其帶有的參數多是隨機性的統計參數,如sid=123sss, sid=234fff ,尤爲是好比商品詳情的id字段等,這樣每一個頁面都自動加入到manifest中,將會帶來很大的存儲開銷,並且是毫無心義的;
因此伴隨而來的,是如何在現有的體系架構下進行數據統計的難題,
對於第一個問題 常規方案是進入離線存儲頁面後自動發出ajax請求,以告知服務器統計PV UV;
對於第二個問題,是將GET請求方式改爲POST方式。
離線存儲的適用場景
1.單頁應用
2.對實時性要求不高的業務
3.webApp
參考連接
[1]http://www.zhangxinxu.com/wordpress/2013/05/caching-tutorial-for-web-authors-and-webmasters/
[2]http://www.codeceo.com/article/http-cache-control.html
[3]http://www.cnblogs.com/Joans/p/3956490.html
[4]http://web.jobbole.com/86970/
[5]http://www.jianshu.com/p/1a9268594deb
[6]http://blog.techbeta.me/2016/02/http-cache/
[7]http://hahack.com/wiki/sundries-http-web.html
[8]http://www.jianshu.com/p/99dc1f8f62bf