在對頁面的性能優化時,特別是移動端的優化,緩存是很是重要的一環。
瀏覽器緩存機制設置衆多:html5 appcache,Expires,Cache-control,Last-Modified/If-Modified-Since,Etag/If-None-Match,max-age=0/no-cache...,
以前對某一個或幾個特性瞭解一二,可是混在一塊兒再加上瀏覽器的行爲,就迷(meng)糊(bi)了.javascript
下面從實現一個簡單的靜態服務器角度,一步一步說瀏覽器的緩存策略。css
對http請求來講,客戶端緩存分三類:html
時間寶貴,如下是最終的流程圖:html5
源碼和流程圖源文件在githubjava
瀏覽的緩存的依據是server http response header , 爲了實現對http response 的徹底控制,用nodejs實現了一個簡單的static 服務器,得益於nodejs簡單高效的api,
不到60行就把一個可用的版本實現了:源碼
可克隆代碼,分支切換到step1, 進入根目錄,執行 node app.js
,瀏覽器裏輸入:http://localhost:8888/index.html,查看response header,返回正常,也沒有用任何緩存。
服務器每次都要調用fs.readFile方法去讀取硬盤上的文件的。當服務器的請求量一上漲,硬盤IO會成爲性能瓶頸(設置了內存緩存除外)。node
response header: HTTP/1.1 200 OK Content-Type: text/html Date: Fri, 03 Jun 2016 14:15:35 GMT Connection: keep-alive Transfer-Encoding: chunked
對於指定後綴文件和過時日期,爲了保證可配置。創建一個config.js。nginx
exports.Expires = { fileMatch: /^(gif|png|jpg|js|css|html)$/ig, maxAge: 60*60*24*365 };
爲了把緩存這個職責獨立出來,咱們再新建一個cache.js,做爲一箇中間件處理request.git
加上超期時間,代碼以下github
module.exports = function (request, response) { var pathname = url.parse(request.url).pathname; var ext = path.extname(pathname); ext = ext ? ext.slice(1) : 'unknown'; if (ext.match(config.Expires.fileMatch)) { var expires = new Date(); expires.setTime(expires.getTime() + config.Expires.maxAge * 1000); response.setHeader("Expires", expires.toUTCString()); response.setHeader("Cache-Control", "max-age=" + config.Expires.maxAge); } }
這時咱們刷新頁面能夠看到response header 變爲這樣了:web
HTTP/1.1 200 OK Expires: Sat, 03 Jun 2017 15:07:23 GMT Cache-Control: max-age=31536000 Content-Type: text/html Date: Fri, 03 Jun 2016 15:07:23 GMT Connection: keep-alive Transfer-Encoding: chunked
多了expires,但這是第一次訪問,流程和上面同樣,仍是須要從硬盤讀文件,再response
再刷新頁面,能夠看到http header :
Request URL:http://127.0.0.1:8888/index.html Request Method:GET Status Code:200 OK (from cache) Remote Address:127.0.0.1:8888
可是到這裏遇到一個問題,並無達到預期的效果,並無從緩存讀取
緩存並無生效。
GET /index.html HTTP/1.1 Host: 127.0.0.1:8888 Connection: keep-alive Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36 Accept-Encoding: gzip, deflate, sdch Accept-Language: zh-CN,zh;q=0.8
查看request header 發現 Cache-Control: max-age=0,瀏覽器強制不用緩存。
瀏覽器會針對的用戶不一樣行爲採用不一樣的緩存策略:
Chrome does something quite different: 'Cache-Control' is always set to 'max-age=0′, no matter if you press enter, f5 or ctrl+f5. Except if you start Chrome and enter the url and press enter.
其它的瀏覽器特性能夠查看文末的【迷之瀏覽器】
因此添加文件entry.html,經過連接跳轉的方式進入就能夠看到cache的效果了。
瀏覽器在發送請求以前因爲檢測到Cache-Control和Expires(Cache-Control的優先級高於Expires,但有的瀏覽器不支持Cache-Control,這時採用Expires),
若是沒有過時,則不會發送請求,而直接從緩存中讀取文件。
Cache-Control與Expires的做用一致,都是指明當前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數據仍是從新發請求到服務器取數據。
只不過Cache-Control的選擇更多,設置更細緻,若是同時設置的話,其優先級高於Expires。
代碼詳細可查看源碼:https://github.com/etoah/BrowserCachePolicy/tree/step2
除了Expires 和Cache-Control 兩個特性的緩存可讓browser徹底不發請求的話,別忘了還有一個html5的新特性 Application Cache
,
在個人另外一篇文章中有簡單的介紹HTML5 Application cache初探和企業應用啓示.
同時在本身寫的代碼編輯器中,也用到了此特性,可離線查看,坑也比較多。
爲了消除 expires cache-control 的影響,先註釋掉這兩行,並消除瀏覽器的緩存。
// response.setHeader("Expires", expires.toUTCString()); //response.setHeader("Cache-Control", "max-age=" + config.Expires.maxAge);
新增文件app.manifest,因爲appcache 會緩存當前文件,咱們可不指定緩存文件,只需輸入CACHE MANIFEST
,並在entry.html引用這個文件。
<html lang="en" manifest="app.manifest">
在瀏覽器輸入:http://localhost:8888/entry.html,能夠看到appcache ,已經在緩存文件了:
從瀏覽器的Resources標籤也能夠看到已緩存的文件:
這時再刷新瀏覽器,能夠看到即便沒有 Expires 和Cache-Control 也是 from cache ,
而index.html 因爲沒有加Expires ,Cache-Control和appcache 仍是直接從服務器端取文件。
這時緩存的控制以下
本例子的源碼爲分支 step3:代碼詳細可查看源碼
Last-Modified/If-Modified-Since。
因此咱們須要把 Cache-Control 設置的儘量的短,讓資源過時:
exports.Expires = { fileMatch: /^(gif|png|jpg|js|css|html)$/ig, maxAge: 1 };
同時須要識別出文件的最後修改時間,並返回給客戶端,咱們同時也要檢測瀏覽器是否發送了If-Modified-Since請求頭。若是發送並且跟文件的修改時間相同的話,咱們返回304狀態。
代碼以下:
fs.stat(realPath, function (err, stat) { var lastModified = stat.mtime.toUTCString(); var ifModifiedSince = "If-Modified-Since".toLowerCase(); response.setHeader("Last-Modified", lastModified); if (request.headers[ifModifiedSince] && lastModified == request.headers[ifModifiedSince]) { response.writeHead(304, "Not Modified"); response.end(); } })
若是沒有發送或者跟磁盤上的文件修改時間不相符合,則發送回磁盤上的最新文件。
一樣咱們清緩存,刷新兩次就能看到效果以下:
服務器請求確認了文件是否新鮮,直接返回header, 網絡負載特別較小:
這時咱們的緩存控制流程圖以下:
本例子的源碼爲分支 step4:代碼詳細可查看源碼:https://github.com/etoah/BrowserCachePolicy/tree/step4
除了有Last-Modified/If-Modified-Since組合,還有Etag/if-None-Match,
ETag ,全稱Entity Tag.
你可能會以爲使用Last-Modified已經足以讓瀏覽器知道本地的緩存副本是否足夠新,爲何還須要Etag(實體標識)呢?HTTP1.1中Etag的出現主要是爲了解決幾個Last-Modified比較難解決的問題:
在node 的後端框架express 中引用的是npm包etag,etag 支持根據傳入的參數支持兩種etag的方式:
一種是文件狀態(大小,修改時間),另外一種是文件內容的哈希值。
詳情可相看etag源碼
由上面的目的,也很容易想到怎麼簡單實現,這裏咱們對文件內容哈希獲得Etag值。
哈希會用到node 中的Crypto模塊 ,先引用var crypto = require('crypto');
,並在響應時加上Etag:
var hash = crypto.createHash('md5').update(file).digest('base64'); response.setHeader("Etag", hash); if ( (request.headers['if-none-match'] && request.headers['if-none-match'] === hash) // || // (request.headers[ifModifiedSince] && new Date(lastModified) <= new Date(request.headers[ifModifiedSince])) ) { response.writeHead(304, "Not Modified"); response.end(); return; }
爲了消除 Last-Modified/If-Modified-Since的影響,測試時能夠先註釋此 header,這裏寫的是 strong validator,詳細可查看W3C ETag
第二次訪問時,正常的返回304,並讀取緩存
更改文件,etag發生不匹配,返回200
還有一部份功能特性,因爲支持度不廣(部份客戶端不支持(chrome,firefox,緩存代理服務器)不支持,或主流服務器不支持,如nginx, Appache)沒有特別的介紹。
到這裏最終主要的瀏程圖已完畢,最終的流程圖:
最終代碼可查看源碼
每一個瀏覽器對用戶行爲(F5,Ctrl+F5,地址欄回車等)的處理都不同,詳細請查看Clientside Cache Control
如下摘抄一段:
So I tried this for different browsers. Unfortunately it's specified nowhere what a browser has to send in which situation.
這只是一篇原理或是規則性的文章,初看起來比較複雜,但現實應用可能只用到了不多的一部份特性就能達到較好的效果:
咱們只需在打包的時候用gulp生成md5戳或時間戳,過時時間設置爲10年,更新版本時更新戳,緩存策略簡單高效。
關於緩存配置的實戰這些問題,
好比,appcache,Expires/Cache-Control 都是不需發任何請求,適用於什麼場景,怎麼選擇?
配置時,不是配置express,配的是nginx,怎麼配置 ,下篇《詳說瀏覽器緩存-實戰篇》更新。
W3C ETag
rfc2616
What takes precedence: the ETag or Last-Modified HTTP header?
出處:http://www.cnblogs.com/etoah/ 歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面保留此段聲明。