在对页面的性能优化时,特别是移动端的优化,缓存是很是重要的一环。
浏览器缓存机制设置众多: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/ 欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面保留此段声明。