为何要使用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