JavaScript权威指南--客户端存储
客户端存储web应用允许使用浏览器提供的API实现将数据存储在用户电脑上。
客户端存储遵循“同源策略”,因此不同站点的页面是无法读取对于存储的数据。而同一站点的不同的页面之间是可以互相共享存储数据的。
同源策略可以拿localStorage来讲,如下:
打开http://www.w3school.com.cn/
打开http://www.w3school.com.cn/html5/index.asp
数据都是保存在对应的域名http://www.w3school.com.cn/下的,当域名同源的时候,是可以访问到保存下来的数据的。
web应用可以选择他们存储数据的有效期:有临时存储的,可以让数据保存至当前窗口关闭或浏览器退出;也有永久存储,可以将数据永久地存储在硬盘上,数年或者数月不失效。
客户端存储有以下几种形式:
web存储
先是HTML5的一个API,后来成了标准了。目前包含IE8在内的主流浏览器(可交互地)都实现了。包含localStorage和sessionStorage对象,这两个对象实际上是持久化的关联数组,是名值对的映射表,“名”和“值”都是字符串。web存储易于使用,支持大容量(但非无限量)数据存储同时兼容当前所有主流浏览器(但不兼容早期浏览器)
cookie
cookie是一种早期的客户端存储机制,期初是为征对服务器端脚本设计使用的。尽管在客户端提供了非常繁琐的javascript API来操作cookie,但它们难用至极,而且只适合少量的文本存储。不仅如此,任何以cookie形式存储的数据,不论服务器端是否需要,每一次HTTP请求都要把这些数据传输到服务器端。cookie目前仍然被大量的客户端程序员使用的一个重要的原因是:所有新旧浏览器都支持它。但是,随着Web Storage的普及,cookie将最终回归到最初的形态:作为一种被服务端脚本使用的客户存储机制。
IE User Data
微软在IE5之后的IE浏览器实现了专属它的客户端存储机制-“User Data”。userData可以实现一定量的字符串数据存储,对于IE8前的IE浏览器,可以将其用做是Web存储的替代方案。
离线web应用
HTML5标准定义了一组“离线web应用”API,用于缓存web页面以及相关资源(图片,脚本,CSS文件等)。它的现实是将web应用整体存储在客户端,而不仅是存储数据。它能够让web应用“安装”在客户端。这样一来,哪怕网络不可用的时候web应用依然是可用的。
web数据库
为了能够让开发者像使用数据那样操作大量的数据,很多主流浏览器纷纷实现在其中开始集成客户端数据库功能。Safari,Chrome,和Opera都内置了SQL数据库的客户端API。遗憾的是这类的API标准化最终以失败而告终,并且,Firefox和IE看来不打算实现这类API。目前还有一种正在标准化的API,称为“索引数据库API”。调用该API返回的是一个不包含查询术语的简单数据库对象。这两种客户端数据库API都是异步的,都使用了事件处理机制(类似DOM事件的机制),这样的方式多多少少都显得复杂。
文件系统API
前面介绍过现代的浏览器都支持一个文件对象,用以将选择的文件通过xmlHttPRequest上传到服务器。与之相关的规范(草案阶段)定义了一组API,用于操作一个私有的本地文件系统。该系统中,可以进行对文件的读写操作。目前正在标准化当中。
存储、安全和隐私
web浏览器通常会提供“记住密码”功能,这些密码会以加密的形式安全地存储在硬盘上。然而本章介绍的任何形式客户端数据存储都不牵扯加密:任何存储在用户硬盘上的数据都是未加密形式。这样一来,对于拥有电脑访问权限的恶意用户或者恶意软件同样也可以获得存储的数据。因此,任何形式的客户端存储都不应该用来保存密码、商业账号或者类似的敏感信息。
还要记住一点:很多web用户不信任那些使用cookie和其它客户端存储机制来做类似“跟踪”功能的网站。所以,尽量尝试本章讨论的存储机制来网站升级用户体验;而不是用它们来收集和侵犯隐私相关的数据。如果网站滥用客户端存储,用户将禁用该功能。这样一来不仅达不到效果,还会导致依赖客户端存储的网站完全不可用。
1.localStorage和sessionStorage
实现了“web存储”草案标准的浏览器在window对象上定义了两个属性,localStorage和sessionStorage。在控制台输入window.localStrage可以获得页面所存储的数据。
这两个属性代表同一个Storage对象——一个持久化关联数组,数组使用字符串来索引,存储的值也是字符串形式的,Storage对象在使用上和一般的Javascript没有什么两样,设置对象的属性为字符串值,随后浏览器会将值存储起来。lcoalStorage和sessionStorage两者的区别在于存储的有效期和作用域不同:数据可以存储多长时间及拥有数据的访问权。
示例:localStorage和sessionStorage类似
var name = localStorage.username; //查询一个存储的值 name = localStorage["username"]; //等于数组表示法 if (!name) { name = prompt("请输入你的名字"); //询问用户一个答案 localStorage.username = name; //存储用户的答案 } //迭代所有存储的name/value对 for (var name in localStorage) { //迭代所有存储的名字 var value = localStorage[name]; //查询每个名字对应的值 }
Storage对象还定义了一些诸如存储、获取、遍历和删除的方法。
“web存储”草案标准指出,我们既可以存储结构化的数据(对象和数组),也可以存储原始类型数据,还可以存储诸如日期、正则表达式甚至是文件对象在内的内置类型和数据。(在一些浏览器中,仅仅支持存储字符串类型数据,如果要存储和其它类型的书,不得不自己手动进行和编码解码),如下例子所示:
//当存储一个数字的时候,会自动转换成一个字符串 //但是,当获取该值的时候,别忘了手动转换成数字类型 localStorage.x = 10; var x = parseInt(localStorage.x); //同样,存储一个日期类型数据的时候进行编码,获取的时候进行解码 localStorage.lastRead = (new Date()).toUTCString(); var lastdate = new Date(Date.parse(localStorage.lastRead)); //使用JSON可以使得对基本数据类型的编码规则变得很方法 var data = new Date(); localStorage.data = JSON.stringify(data)//编码后存储 var data = JSON.parse(localStorage.data)//获取数值后再解码
1.1.存储有效期和作用域
通过localStorage存储的数据是永久性的,除非web应用立刻删除存储的数据。或者用户通过设置浏览器配置(浏览器提供的特定UI)来删除,否则数据将一直保留在用户电脑上,永不过期。
localStorage的作用域是限定在文档源(document origin)级别的。文档源是通过协议、主机名及端口三者来确定的。因此,每个url都有不同的文档源。
同源文档间共享localStorage数据(不论该源的脚本是否真正的访问的localStorage)。它们可以互相读取对方的数据,甚至可以覆盖对方的数据。但是,非同源的文档见互相都不能读取和共享或覆盖对方的数据(即使它们的脚本来自同一台第三方服务器也不行)。
需要注意的是localStorage的作用域也收到浏览器供应商的限制。如果你使用的Firefox访问站点那么下一次用另外一个浏览器打开的时候,那么本次是无法获取上次的数据的。
sessionStorage的有效期和存储数据的脚本所在最顶层的窗口或者是浏览器标签页是一样的。一旦窗口或者标签页被永久关闭了,那么所有通过sessionStorage存储的数据也被删除了(需要注意的是,现代的浏览器具备了一些关闭标签页随后恢复上一次浏览器回话的功能,可能预知相关的sessionStorage会更长些)。
作用域也是限定在文档源中,因此,非同源的文档之间是无法贡献sessionStorage的。不仅如此,sessionStorage的作用域还被限定在窗口中。如果同源的文档渲染在不同的浏览器标签页中,那么他们互相之间拥有的是各自的sessionStorage数据,无法共享。就是说,同个浏览器下两个相同的标签页也不能共享数据。
需要注意的是:这里提到的基于窗口作用域的sessionStorage指的窗口是顶级窗口。如果一个浏览器标签包含两个<iframe>元素,它们是同源的,那么两者之间是可以共享的sessionStorage的。
1.2.存储API
localStorage和sessionStorage通常是被当做普通的javascript对象使用。通过设置属性来存储字符串值,查询该属性来读取该值。这两个对象还提供了两个正式的API。
setItem()
getItem()
removeItem()(在非IE8的浏览器中,还可以使用delete操作符来删除数据,就和普通对象使用的delete操作符一样)
clear()(不需要参数)
使用length和key()方法,传入0~length~1的数字,可以枚举所有存储数据的名字。
示例:
localStorage.setItem("x", 1); //以"x"的名字存储一个数值 localStorage.getItem("x"); //获取x的数值 //枚举所有的存储的名字/值对 for(var i = 0;i<localStorage.length;i++){//length 表示了所有名字/值对 var name = localStorage.key(i); //获取第i对的名字 var value = localStorage.getItem(name);//获取该对的值 } localStorage.removeItem("x")//删除"x"项 localStorage.clear();//全部删除
如果浏览器提供商完全实现了“web存储”的标准,只对对象和数组类型的数据存储,那么就会又多了一个使用类似用setItem()和getItem()这类方法的理由。对象和数组的值通常是可变的,因此存储对象要求存储它们的副本,以确保之后对任何这类对象的改变不影响到存储的对象。同样的 ,在获取该对象的时候,也要求获取是该对象的副本,以确保已获取的对象的改动不会影响到存储的对象。而这类操作如果如果使用基于属性的API就会令人困惑。考虑如下这段代码(假设浏览器已经支持了结构化数据存储)。
localStorage.o = {x:1};//存储一个带有"x"属性的对象 localStorage.o.x = 2; //试图设置该对象的属性值 localStorage.o.x; //=>undefined x没有改变
......
目前还没完全实现这个标准呢。
1.3、存储事件
无论什么时候,存储在localStorage或者sessionStorage的数据发生改变,浏览器都会在其它对该数据可见的窗口对象上出发存储事件(但是在对数据改变的窗口上是不出发的)。如果浏览器有两个标签页都打开了来自于同源的页面,其中一个在localStorage上存储了数据,那么在另外一个标签页就会接受到一个存储事件。要记住,sessionStorage的作用域是限制在顶层窗口的,因此对于sessionStorage的改变只有相关牵连的窗口才会触发存储事件。像给已经存在的存储项设置一个一模一样的值,或者是删除一个本来就不存在的项存储都是不会触发存储事件的。
为存储事件注册处理程序可以通过addEventListener()方法,(或者在IE下使用attachEvent方法)。在绝大多浏览器中,还可以给Window对象设置onstorage属性的方法,不过Firefox不支持该属性。
与事件相关的事件对象有5个非常重要的属性(遗憾的是IE8不支持他们)
key:被设置或移除项的键名。如果调用clear()函数,那么该属性值就为null.
newValue:保存该项新值;或者调用removeItem()时,该属性值为null。
oldvalue:改变或删除该项前,保存该项的原先值;当插入一个新项的时候,该属性值为null
storageArea:这个属性值就好比目标window对象上的locaStorage属性或者是sessionStorgae属性
url:触发该存储变化脚本所在的文档URL
最后,要注意的是:localStorage和sessionStorage存储机制都是广播机制的,浏览器会对目前正在访问的同源站点所有窗口发送消息。举个例子:如果一个用户要求网站停止动画效果时,那么该站点可能会在localStorage中存储该用户的首选项,这样依赖,以后再访问该站点的时候自动停止该动画效果了。因为存储了首选项,导致触发的一个存储事件让其它的同源页面也获得了一个用户请求。再比如,一个机遇web的图片编辑应用,通常允许在其它窗口中展现工具条。当用户选择一个工具的时候,应用就可以使用localStorage来存储用户的当前状态,然后通知其它的窗口用户选择了新的工具。
2.cookies
cookie是指web浏览器存储少量的数据,同时它是与具体的web页面或者站点相关的。cookie是最早是设计为被服务端所有的,从底层来看,作为HTTP协议的一种扩展实现它。cookie数据会自动在web浏览器和web服务器之间传输的,因此服务端脚本就可以读、写客户端的cookie值。
可以通过document.cookie来获取cookie值。
为什么叫“cookie”?
并没太多含义。在javascript中,cookie用于保存状态以及能够为web浏览器提供一种身份识别(密码等)。但是,javascript中使用cookie不会采取任何加密机制,因此是不安全的。(但是通过https传说cookie数据是安全的,不过这和cookie本身无关,而和https:协议相关)。
操作cookie的api很早就已经定义和实现了,因此该API的兼容性很好。但是,该API几乎形同虚设。根本没有提供诸如查询、设置、删除cookie对象的方法,所有这些操作都要通过特殊格式的字符串形式读写Document对象的cookie属性来完成。每个cookie的有效期和作用域都可以通过cookie属性分别指定。这些属性也是通过在同一个cookie上以特殊的字符串来设定的。
检测cookie是否可用
由于滥用第三方cookie(如cookie是和网页上的图片相关而非网页本身相关。第三方cookie指的是来自于当前访问站点以为的站点设置的cookie)的缘故,导致cookie在大多数web用户心中有不好的印象。比如,广告公司可以利用第三方cookie来实现跟踪用户的访问行为和习惯,而用户为了禁止这种“窥探”用户隐私的行为会在它们的浏览器中禁用cookie。因此在使用avascript操作cookie前,首先要确保cookie是启用的。在绝大多数浏览器中,可用通检测过navigator.cookieEnabled这个属性来实现,若为true则表明cookie是启用的。因为该属性不是一个标准的属性,所以在不支持该属性的浏览器上,要用下面介绍的cookie技术来读、写和删除cookie数据来测试是否支持cookie。
2.1.cookie属性:有效期和作用域
除了名(name)和值(value),cookie还有一些可选的属性来控制cookie的有效期和作用域。cookie的默认有效期很短暂;它只在持续在web会话期间,一旦关闭浏览器,cookie的保存数据就丢失。需要注意的是这与sessionStorage有效期还是有区别的,cookie的作用域并不是限在浏览器的单个窗口中,它的有效期和整个浏览器进程不是单个浏览器窗口的有效期一一致。如果要延长cookie有效期,可以通过设置max-age属性,但必须要明确告诉cookie的有效期是多长,单位是秒。
一旦设置了有效期,浏览器就会将cookie数据存储在一个文件中,并且知道过了指定的有效期才会删除该文件。
和localStorage以及sessionStorage类似,cookie的作用域是通过文档源和文档路径来确定的。该作用域通过cookie的path和domain属性配置的。在默认的情况下,cookie和创建它的web页面有关,并对该web页面及和该web页面同目录或者子目录其它的页面可见。比如web页面http://www.a.com/a/index.html页面创建了一个cookie,那么该cookie对http://www.a.com/a/b.html和页面http://www.a.com/a/b/index.html页面都是可见的。但对http://www.a.com/c.html是不可见的。
默认的cookie的可见性行为满足了最常见的需求。不过,有的时候,你可能希望整个网站都使用cookie的值,而不管是那个页面创建它的。比如用户在一个页面表单上输入了它的邮件地址,你想把它保存下来,为了不让用户下次回到这个页面上将其作为默认的邮件地址。要满足这样的需求,可以设置cookie的路径(设置cookie的path属性)。
将cookie的路径设置成"/"等于是让cookie和localStorage拥有同样的作用域,同时当它请求该站点上任何一个web页面的时候,浏览器都必须将cookie的名字和值传递给服务器。但是需要注意的是,cookie的path属性不能被用做访问控制机制。如果一个web页面想要读取同一站点其它页面的cookie,只需要将其他页面隐藏<iframe>形式加载进来,随后读取对应文档的cookie就可以了。同源策略限制了跨站的cookie窥探,但是对同一站点的文档是完全合法的。
cookie的作用域默认由文档源限制。但是,有的大型网站想要子域之间能够相互通信cookie,这个时候就需要设置coolkie的domian属性来达到目的。如果http://well.a.com的域下创建了一个cookie,并将其path属性设置成"/",其domian属性设置成".a.com",那么这个cookie就对所有good.a.com,other.a.com以及任何其它a.com域下任何其它服务器都可见。如果没有为一个cookie设置域属性,那么domian属性的默认值是当前web服务器的主机名。要注意的是,cookie域只能设置当前服务器的域。
最后要介绍的cookie的属性是secure,它是一个布尔值属性,用来表明cookie的值是以何种网络传输。cookie默认是以不安全的形式(通过普通的、不安全的HTTP连接)传递的。一旦cookie被标识为“安全的”,那就只能当前浏览器和服务器通过https或者其它安全协议连接的时候才能传递它。
2.2.保存cookie
要给当前文档设置有效的cookie值,非常简单,只需将cookie属性设置为一个字符串的形式的值;
name = value;
如下:
document.cookie = "version=" + encodeURIComponent(document.lastModified);
下次读取cookie属性的时候,之前存储的名/值对的数据就在cookie列表中。由于cookie的名/值中不允许包含分号、逗号和空白符,因此,在存储之前一般可以采用javascript核心的全局函数encodeURIComponent()进行编码。相应的,读取cookie值的时候需要采用decodeURICompoent()函数解码。
以简单的名/值对形式存储的cookie数据有效期只在当前的web浏览器会话内,一但用户关闭浏览器,cookie数据就丢失了。如果小要延长cookie的有效期,就需要设置max-age属性来指定cookie的有效期(单位是秒)。按照下面的字符串形式设置cookie的属性即可。
name = value; max-age = seconds
下面设置一个cookie值,并提供一个max-age属性
//以名/值对形式存储cookie //同时采用encodeURIComponet()函数进行编码,来转义分号、逗号和空白符 //如果dayTolive是一个数字,设置max-age属性为该数值表示cookie知道指定的天数 //到了才会过期。如果daysTolive是0就表示删除cookie function setcookie(name, value, daysTolive) { var cookie = name + "=" + encodeURIComponent(value); if (typeof daysTolive === "number") cookie += "; max-age =" + (daysTolive * 60 * 60 * 24); document.cookie = cookie; } setcookie("cook","cool",0);
同样地,如果要设置cookie的path、domian和secure属性,只需如下的字符串追加在cookie的值的后面
;path = path ;domain = domian ;secure
要改变cookie的值,需要使用相同的名字、路径和域,但是新的值重新设置cookie的值。同样地,设置新max-age属性就可以改变cookie的有效期。
要删除一个cookie,需要使用相同的名字、路径和域,然后制定一个任意(非空)的值,并将max-age属性指定为0,再次设置cookie。
2.3.读取cookie
使用javascript表达式来读取cookie属性的时候,其返回的值是一个字符串,该字符串都是由一系列的名/值对组成,不同名/值对之间通过“分号/空格”分开,其内容包含了所有作用在当前文档的cookie。但是当它不包含其它设置的cookie属性。通过document.cookie属性可以获取cookie的值,但是为了更好地查看cookie值,一般会采用split()的方法将cookie值中的名/值对都分离出来。
把cookie的值从cookie属性分离出来之后,必须要采用相应的解码方式(取决于之前存储cookie值时的采用的编码方式),把值还原出来。比如,先采用decodeURIComponent()方法把cookie的值解码出来,之后再利用JSON.parse()方法转换成json对象。
下面的例子定义了一个getcookie()函数,该函数将document.cookie属性的值解析出来,将对应的名/值存储到一个对象中,函数最后返回该对象。
/**解析document.cookie属性的值**/ //将document.cookie的值以名/值对组成一个对象返回 //假设存储cookie的值的时候是采用encodeURIComponent()编码的 function getcookie() { var cookie = {}; //初始化要返回的对象 var all = document.cookie; //在一个大写的字符串中获取所有的cookie值 if (all === "") //如果cookie为空字符串 return cookie; //返回一个空对象 var list = all.split(";"); //分离出名/值对 for (var i = 0; i < list.length; i++) { //遍历每个cookie var cookie = list[i]; var p = cookie.indexOf("="); //查找第一个"="符号 var name = cookie.substring(0, p); //获取cookie名字 var value = cookie.substring(p + 1); //获取cookie对应的值 value = decodeURIComponent(value); //对其值进行解码 cookie[name] = value; //将名/值对存储到对象中 } return cookie; }
在wamp下测试,不能用,结果都会直接等于list而不是对象。cookie不能直接用啦,修改如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script type="text/javascript"> function setcookie(name, value, daysTolive) { var cookie = name + "=" + encodeURIComponent(value); if (typeof daysTolive === "number") cookie += "; max-age =" + (daysTolive * 60 * 60 * 24); document.cookie = cookie; } setcookie("cook","cool",60); function getcookie() { var cookie = {}; //初始化要返回的对象 var all = document.cookie; //在一个大写的字符串中获取所有的cookie值 if (all === "") //如果cookie为空字符串 return cookie; //返回一个空对象 var list = all.split(";"); //分离出名/值对 for (var i = 0; i < list.length; i++) { //遍历每个cookie var cookieEle = list[i]; var p = cookieEle.indexOf("="); //查找第一个"="符号 var name = cookieEle.substring(0, p); //获取cookie名字 var value = cookieEle.substring(p + 1); //获取cookie对应的值 value = decodeURIComponent(value); //对其值进行解码 cookie[name] = value; //将名/值对存储到对象中 } return cookie; } console.log(getcookie()); </script> </body> </html>
2.4.cookie的局限性
cookie的设计初衷是给服务端脚本用来存储少量的数据的,该数据会在每次请求一个相关的URL时传递到服务器中。RFC2965鼓励浏览器供应商对cookie的数据大小不做限制。可是,要知道,该标准不允许浏览器保存超过300个cookie,为每个web服务器保存的cookie数不能超过20个(是对整个服务器而言,而不仅仅指服务器上的页面和站点),而且,每个cookie保存的数据不能超过4k。实际上,现代浏览器允许cookie总数超过300个,但是部分浏览器对单个cookie大小仍然有4k的限制。
2.5.cookie相关存储
例:定义一个cookieStorage函数,通过将max-age和path属性传递给该构造函数,通过将max-age和path属性传递给该构造函数,就会返回一个对象,然后就可以像使用localStorage属性或sessionStorage一样来使用这个对象。但是要注意的是,该例并没有实现存储事件,因此,当设置和查询cookieStorage对象的属性的时候,不会实现自动保存和获取对应的值。
实现基于cookie的API。
3.利用IE userdata持久化数据
IE以及IE5以上的浏览器是通过在document元素后附件一个专属的"DHTML行为"来实现客户端存储的。如下代码所示:
var memory = document.createElement("div"); //创建一个元素 memory.id = "_memory"; memory.style.display = "none"; memory.style.behavior = "url('default#userData')"; //附加userData行为 document.body.appendChild(memory); //添加到document元素中
一旦元素赋予了"userData"行为,该元素就拥有load()和save()方法。load()方法用于载入存储的数据。使用它的时候必须传递一个字符串作为参数--类似一个文件名,该参数用来指定要载入的存储数据。当数据载入后,就可以通过该元素的属性来访问这些名/值对形式的数据,可以使用gettAttribute()来查询摄像数据。通过setAttribute()方法设置属性。然后调用save()方法可以存储新的数据;而要删除数据,通过使用removeAttribute()方法然后调用save()方法即可。如下所示(该例使用了此前例子中初始化的那个memory元素):
memory.load("mySoredData"); //根据指定名,载入对于的数据 var name = memory.getAttribute("username"); //获取其中数据片段 if (!name) { //如果没有指定的数据片段 name = prompt("请输入"); // memory.setAttribute("username", name); //将其设置成memory元素的一个属性 memory.save("mySoredData") }
默认情况下,通过userData存储的数据,除非手动去删除它否则永不失效。但是,也可以通过设置expires属性来指定它的过期时间。就拿上面的例子来说,可以给存储的数据设置时长为100天的有效期,如下所示:
var now = (new.Date()).getTime(); //获取当前时间,以毫秒为单位 var expires = now + 100 * 24 * 60 * 60 * 1000; //100天的毫秒算法 expires = new Date(expires).toUTCString(); //将其转换为字符串。 memory.expires = expires; //设置userData的过期时间
IE userData的作用域限制在和当前文档同目录的文档中。它的作用域没有cookie宽泛,cookie对其所在目录下的子目录也有效。userData的机制并没有像cookie那样,通过设置path和domain属性来控制或改变其作用域的方式。
userData允许存储的数据量要比cookie大,但是却比localStorage和sessionStorage允许的存储量的数据量要小。
下面的例子基于IE的userData实现存储了API提供的getItem(),setItem()以及removeItem()方法(但是它没有实现key()或者clear()方法,原因是userData并没有定义遍历所有存储项的一种方法)。
。。。。。。
由于上述代码只在IE浏览器下有效,最好使用IE条件注释来避免其他浏览器载入上述代码。
<!--[if IE]> <script src="userDataStorage.js"></script> <![endif]-->
4.应用程序存储和离线Web应用
HTML5中新增了“应用程序缓存”,允许web应用程序自身本地保存到用户的浏览器中。不像localStorage和sessionStorage只是保存web应用程序相关的数据,它是将应用程序自身保存起来----应用程序所需运行的所有文件(HTML、CSS、JavaScript、图片等)。“应用程序缓存”和一般的浏览器缓存不同:它不会随着用户清除浏览器缓存而被清除。同时,缓存起来的应用程序也不会像一般固定大小的缓存那样,老数据会被最近一次访问的新数据代替掉。它其实不是临时存储在缓存中:应用程序更像是被“安装”在那里,除非被用户“卸载”或“删除”它们,否则,它们就会一直“驻扎”在那里。所以说,“应用程序缓存”在真正意义上不是“缓存”更好的说法应该称之为“应用程序存储”。
让web应用能够实现“本地安装”的目的是要保证它们能够在离线状态(比如,当在飞机或者手机没信号的时候)下依然可访问。将自己“安装”到应用程序缓存中的Web应用,在离线状态下使用localStorage来保存应用相关的数据,同时还具备一套同步机制,在再次回到在线状态的时候,能够将存储的数据传输给服务器。
4.1.应用程序缓存清单
想要将应用程序“安装”到应用程序缓存中,首先要创建一个清单:包含所有应用程序依赖的所有url列表。然后,通过在应用程序主HTML页面的<html>标签中设置manifest属性,指向到清单文件就可以了:
<!DOCTYPE HTML> <html manifest=”myapp.appcache”> <head>…</head> <body>…</body> </html>
清单文件中的首行内容必须以”CACHE MANIFEST”字符串开始。其余就是要缓存的文件URL列表,一行一个URL。相对路径的URL都相对于清单文件的URL 。会忽略内容中的空行,会作为注释而忽略以”#”开始的行。注释前面可以有空格,但同在同一行注释后面是不允许有非空字符串的。
比如:
CACHE MANIFEST
#上面一行标识此文件是一个清单文件。本行是注释
# 下面的内容都是应用程序依赖的资源文件的URL
myapp.html
myapp.js
myapp.css
images/background.png
补充:
缓存清单的MIME类型:
应用程序缓存清单文件约定以.appcache作为文件扩展名。但是,这也仅仅只是约定而已,web服务器真正识别清单文件的方式是通过”text/cache-manifest”这个MIME类型的一个清单。如果服务器将清单文件的Content-Type的头信息设置成其他MIME类型,那么就不会缓存应用程序了。因此,可能需要对web服务器做一定的配置来使用这个MIME类型,比如,在Web应用目录下创建Apache服务器的一个.htaccess文件
清单文件包含要缓存的应用的标识。如果一个Web应用有很多web页面,那么每个html页面就需要设置<html manifest=>属性来指向清单文件。事实上,将这些不同的页面都指向同一个清单文件,可能很清楚地表达出它们都是需要缓存起来的,同时它们又是来自同一个web应用的。如果一个应用只有少量的html页面,那么一般会把这些页面都显示地列在清单文件中。但这不是强调的,会认为任何链接到清单文件的文件都是web应用的一部分,并会随着应用一起缓存起来。
像之前提到的,一个简单的清单必须列出web应用依赖的所有资源。一旦一个Web应用首次下载下来并缓存,之后的任何加载请求都来自缓存。从缓存中去载入一个应用资源的时候,就要求它请求的资源务必要在清单中。不会载入不在清单的资源。这种政策有点离线的味道。如果一个简单的缓存起来的应用能够从缓存中载入并运行,那么它也可以在浏览器的离线状态下运行。通常情况下,很多复杂的web应用无法将它们依赖的所有资源都缓存起来。但是,如果它们同时也有一个复杂的清单的话,它们仍然可能使用应用程序缓存。
复杂的清单
清单文件还有比这更复杂的语法,列举资源的方式也还有另外两种。在清单文件中可以使用特殊的区域头(译者注:类似于HTTP头)来标识该头信息之后清单项的类型。像该例中列举的简单缓存项事实上都属于“CACHE:”区域,这也是默认的区域。另外两种区域是以“Network:”和”FALLBACK:” 头信息开始的(一个清单文件可以有任意数量的区域,而且可以相邻两个区域之间可以根据需要相互切换)。
“NETWORK:”区域标识了该区域中的资源从不缓存,总要通过网络获取。通常,会将一些服务端的脚本资源放在”NETWORK:”区域中,而一般该区域中的资源的URL都只是URL前缀,用来表示以此URL前缀开头的资源都应该要通过网络加载。”NETWORK:”区域中的URL还支持”*”通配符。该通配符表示对任何不在清单中的资源,浏览器都将通过网络去获取。这实际上违背了这样一条规则:缓存应用程序必须要在清单中列举所有应用相关的资源!
“FALLBACK:”区域中的清单项每行都包含两个URL。第二个URL是指需要缓存起来的资源,第一个URL是一个前缀。任何能够匹配到该前缀的资源都不会缓存起来,但是可能的话,他们会从网络中载入。如果从网络中载入失败的话,会使用第二个URL指定的资源来代替,从缓存中获取。想象一个应用包含了一定数量的视频教程。这些视频都很大,显然把他们缓存到本地是不合适的。因此,在离线状态下,通过清单文件中的fallback区域,就可以使用一些文本类的帮助文档来代替了。
示例
CACHE MANIFEST CACHE: myapp.html myapp.css myapp.js FALLBACK: videos/ offline_help.html NETWORK: cgi/
4.2.缓存的更新
当一个web应用从缓存中载入的时候,所有与之相关的文件也是直接从缓存中获取。在线状态下,浏览器会异步检查是否清单文件有更新。如果有更新,新的清单文件以及所有清单中列举的文件都会下载下来重新保存到应用程序缓存中。但是,要注意的是,浏览器只是检查清单文件,而不会去检查缓存的文件是否有更新:只检查清单文件。比方说,你修改了一个缓存的JavaScript文件,要想让该文件生效,就必须去更新下清单文件。由于应用程序依赖的文件列表其实并没有变化,因此最简单的方式就是更新版本号:
CACHE MANIFEST # MyApp version 1 (更改这个数字以便让浏览器重新下载这个文件) MyApp.html MyApp.js
同样的,如果想要让web应用从缓存中“卸载”,要在服务器端删除清单文件,使得请求该文件的时候返回HTTP 404 无法找到 的错误,同时,修改HTML文件与该清单列表“断开链接”。
要注意的是,浏览器检查清单文件以及更新缓存的操作是异步的,可能是在从缓存中载入应用之前,也有可能同时进行。因此,对于简单的web应用而言,在你更新清单文件之后,用户必须载入应用两次才能保证最新的版本生效:第一次是从缓存中载入老版本随后更新缓存,第二次才从缓存中载入最新的版本。
浏览器在更新缓存过程中会触发一系列事件,你可以通过注册处理程序来跟踪这个过程同时提供反馈给用户。
......
4.3.离线web应用
离线web应用指的是将自己“安装”在应用程序缓存中的程序,使得哪怕在浏览器处于离线状态时候依然可访问。举个最简单的例子——类似时钟和万花筒生成器这样的应用——这是web应用要离线可用需要做的事情。但是,大多数web用也需要向服务器上传数据:哪怕是简单的游戏应用都有可能需要上传用户的最高得分到服务器。这类应用也可以成为离线应用。他们可以使用localStorage来存储应用数据,然后当在线的时候再将数据上传到服务器。在本地存储和服务器端同步数据是将Web应用转变为离线应用最巧妙的环节,特别是当用户需要从多台设备获取数据的时候。
为了在离线状态可用,web应用需要可以告知别人自己是离线还是在线,同时当网络连接的状态发生改变时候也能“感知”到。通过navigator.onLine属性,可以检测浏览器是否在线,同时,在Window对象上注册在线和离线事件处理程序,可以检测网络连接状态的改变。
一个简单的记事本程序
20170420补充
浏览器对于存储数据的大小有限制,限制的大小普遍在5M/域,超出则会报Uncaught QuotaExceededError错误。因此在开发时应注意控制存储数据保持在限制大小内,并定时清除无用的数据。若要手动删除数据,Chrome浏览器可至chrome://settings/cookies 查看不同网站已使用本地存储的占用空间情况,并进行删除操作。