本篇内容
  • 离线检测
  • 应用缓存
  • 数据存储

支持离线Web应用开发是HTML5的另一个重点。所谓离线Web应用,就是在设备不能上网的情况下仍然可以运行的应用。HTML5把离线应用作为重点,主要是基于开发人员的心愿。前端开发人员一直希望Web应用能够与传统的客户端应用同场竞技,起码做到只要设备有电就能使用。
开发离线Web应用需要几个步骤。首先是确保应用知道设备是否能上网,以便下一步执行正确的操作。然后,应用还必须能访问一定的资源(图像、JavaScript、css等)。只有这样才能正常工作。最后,必须有一块本地空间用于保存数据,无论能否上网都不妨碍读写。HTML5及其相关的API让开发离线应用成为现实。

1 离线检测

开发离线应用的第一步是要知道设备是在线还是离线,HTML5为此定义了一个navigator.onLine属性,这个属性值为true表示设备能上网,值为false表示设备离线。这个属性的关键是浏览器必须知道设备能否访问网络,从而返回正确的值。实际应用中,navigator.onLine在不同浏览器间还有一些小的差异。

  • IE6+和Safari5+能够正确检测到网络已断开,并将navigator.onLine的值转换为false。
  • Firefox3+和Opera 10.6+支持navigator.onLine属性,但你必须手工选中菜单项“文件→Web开发人员(设置)→脱机工作“才能让浏览器正常工作。
  • Chrome 11及之前版本始终将navigator.onLine属性没置为true。这是-个有待修复的bug。

由于存在上述兼容性问题,单独使用navigator.onLine属性不能确定网络是否连通。即使如此,在请求发生错误的情况下,检测这个属性仍然是管用的。以下是检测该属性状态的示例。

1
2
3
4
5
if (navigator.onLine) {
// 正常工作
} else {
// 执行离线状态时的任务
}

除navigator.onLine属性之外,为了更好地确定网络是否可用,HTML5还定义了两个事件: online和offline。当网络从离线变为在线或者从在线变为离线时,分别触发这两个事件。这两个事件在window对象上触发。

1
2
3
4
5
6
7
EventUtil.addHandler(window, 'online', function () {
alert('online');
});

EventUtil.addHandler(window, 'offline', function () {
alert('offline');
});

为了检测应用是否离线,在页面加载后,最好先通过navigator.onLine取得初始的状态。然后,就是通过上述两个事件来确定网络连接状态是否变化。当上述事件触发时,navigator.onLine属性的值也会改变,不过必须要手工轮询这个属性才能检测到网络状态的变化。
支持离线检测的浏览器有IE6+(只支持navigator.onLine属性)、Firefox3, Safari4、Opera10.6、Chrome、iOS3.2版Safari和Android版WebKit。

2 应用缓存

HTML5的应用缓存(application cache),或者简称为appcache,是专门为开发离线Web应用而设计的。Appcache就是从浏览器的缓存中分出来的一块缓存区。要想在这个缓存中保存数据,可以使用一个描述文件(manifest file),列出要下载和缓存的资源。下而是一个简单的描述文件示例。

1
2
3
4
5
CACHE MANIFEST
#Comment

file.js
file.css

在最简单的情况下,描述文件中列出的都是需要下载的资源,以备离线时使用。

要将描述文件与页面关联起来,可以在<html>中的manifest属性中指定这个文件的路径,例如

1
<html manifest="/offline.manifest">

以上代码告诉页面,/offline.manifest中包含着描述文件。这个文件的MIME类型必须是 text/cache-manifest

描述文件的扩展名以前推荐用manifest,但现在推荐的是appcache。

虽然应用缓存的意图是确保离线时资源可用,但也有相应的JavaScriptAPI让你知道它都在做什么。这个API的核心是applicationcache对象,这个对象有一个status属性,属性的值是常量,表示应用缓存的如下当前状态。

  • 0:无缓存,即没有与页面相关的应用缓存。
  • 1:闲置,即应用缓存未得到更新。
  • 2:检查中,即正在下载描述文件并检查更新。
  • 3:下载中,即应用缓存正在下载描述文件中指定的资源。
  • 4:更新完成,即应用缓存已经更新了资源,而且所有资源都已下载完毕,可以通过swapCache()来使用了。
  • 5:废弃,即应用缓存的描述文件已经不存在了,因此页面无法再访问应用缓存。

应用缓存还有很多相关的事件,表示其状态的改变。以下是这些事件。

  • checking:在浏览器为应用缓存资找更新时触发。
  • error:在检查更新或下载资源期间发生错误时触发。
  • noupdate :在检查描述文件发现文件元变化时触发。
  • downloading:在开始下载应用缓存资源时触发。
  • progress:在文件下载应用缓存的过程中持续不断地触发。
  • updateready:在页面新的应用缓存下载完毕且可以通过swapCache()使用时触发。
  • cached:在应用缓存完整可用时触发。

一般来讲,这些事件会随着页面加载按上述顺序依次触发。不过,通过调用update()方法也可以手工干预,让应用缓存为检查更新而触发上述事件。

1
applicationCache.update();

update()一经调用,应用缓存就会去检查描述文件是否更新(触发checking事件),然后就像页面刚刚加载一样,继续执行后续操作。如果触发了cached事件,就说明应用缓存已经准备就绪,不会再发生其他操作了。如果触发了updateready事件,则说明新版本的应用缓存已经可用,而此时你需要调用swapCache()来启用新应用缓存。

1
2
3
EventUtil.addHandler(applicationCache, 'updateready', function () {
applicationCache.swapCache();
});

支持HTML5应用缓存的浏览器有Firefox 3+、Safari 4+、Opera 10.6、Chrome、iOS 3.2+版Safari及Android版WebKit。在Firefox 4及之前版本中调用swapCache()会抛出错误。

3 数据存储

随着Web应用程序的出现,也产生了对于能够直接在客户端上存储用户信息能力的要求。想法很合乎逻辑,属于某个特定用户的信息应该存在该用户的机器上。无论是登录信息、偏好设定或其他数据,Web应用提供者发现他们在找各种方式将数据存在客户端上。这个问题的第一个方案是以cookie的形式出现的。cookie是原来的网景公司创造的。一份题为“PersistentClient State: HTIP Cookies”的标准中对cookie机制进行了阐述。今天,cookie只是在客户端存储数据的其中一种选项。

HTTP Cookie,通常直接叫做cookie,最初是在客户端用于存储会话信息的。该标准要求服务器对任意HTTP请求发送SetCookieHTTP头作为响应的一部分,其中包含会话信息。例如,这种服务器响应的头可能如下:

1
2
3
4
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value

这个HTTP响应设置以name为名称、以value为值的一个cookie,名称和值在传送时都必须是URL编码的。浏览器会存储这样的会话信息,并在这之后,通过为每个请求添加Cookie HTTP头将信息发送回服务器,如下所示:

1
2
3
GET /index.html HTTP/1.1
Cookie: name=value
Other-header: other-header-value

发送回服务器的额外信息可以用于唯一验证客户来自于发送的哪个请求。

1. 限制

cookie在性质上是绑定在特定的域名下的。当设定了一个cookie后,再给创建它的域名发送请求时,都会包含这个cookie。这个限制确保了储存在cookie中的信息只能让批准的接受者访问,而无法被其他域访问。
由于cookie是存在客户端计算机上的,还加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间。每个域的cookie总数是有限的,不过浏览器之间各有不同。如下所示。

  • IE6以及更低版本限制每个域名最多20个cookie。
  • IE7和之后版本每个域名最多50个。IE7最初是支持每个域名最大20个cookie,之后被微软的一个补丁所更新。
  • Firefox限制每个城最多 50 个cookie。
  • Opera限制每个域最多30个cookie。
  • Safari和Chrome对于每个域的cookie数量限制没有硬性规定。

当超过单个域名限制之后还要再设置cookie,浏览器就会清除以前设置的cookie。IE和Opera会删除最近最少使用过的(LRU, Least Recently Used)cookie,腾出空间给新设置的cookie。Firefox看上去好像是随机决定要消除哪个cookie,所以考虑cookie限制非常重要,以免出现不可预期的后果。
浏览器中对于cookie的尺寸也有限制。大多数浏览报都有大约4096B(加减1)的长度限制。为了最佳的浏览器兼容性,最好将整个cookie长度限制在4095B(含4095)以内。尺寸限制影响到一个域下所有的cookie,而并非每个cookie单独限制。
如果你尝试创建超过最大尺寸限制的cookie,那么该cookie会被悄无声息地丢掉。注意,虽然一个字符通常占用一字节,但是多字节情况则有不同。

2. cookie的构成

cookie由浏览器保存的以下几块信息构成。

  • 名称:一个唯一确定cookie的名称。cookie名称是不区分大小写的,所以myCookie和MyCookie被认为是同一个cookie。 然而,实践中最好将cookie名称看作是区分大小写的,因为某些服务器会这样处理cookie。cookie的名称必须是经过URL编码的。
  • :储存在cookie中的字符串值。值必须被URL编码。
  • :cookie对于哪个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含子域(subdomain,如www.wrox.com),也可以不包含它(如.wrox.com,则对于wrox.com的所有子域都有效)。如果没有明确设定,那么这个域会被认作来自设置cookie的那个域。
  • 路径:对于指定域中的那个路径,应该向服务器发送cookie。例如,你可以指定cookie只有从http://www.wrox.com/book中才能访问,那http://www.wrox.com的页面就不会发送cookie信息,即使请求都是来自同一个域的。
  • 失效时间:表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务帮发送这个cookie)。默认情况下,浏览器会话结束时即将所有cookie删除;不过也可以臼己设置删除时间。这个值是个GMT格式的日期(Wdy,DD-Mon-YYYY HH:MM:SS GMT),用于指定应该删除cookie的准确时间。因此,cookie可在浏览器关闭后依然保存在用户的机器上。如果你设置的失效日期是个以前的时间,则cookie会被立刻删除。
  • 安全标志:指定后,cookie只有在使用SSL连接的时候才发送到服务器。例如,cookie信息只能发送给https://www.wrox.com,而http://www.wrox.com的请求则不能发送cookie。

每一段信息、都作为Set-Cookie头的一部分.使用分号加空中各分隔每一段,如下例所示。

1
2
3
4
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com
Other-header: other-header-value

该头信息指定了一个叫做name的cookie,它会在格林威治时间2007年1月22日7:10:24失效,同时对于www.wrox.com和wrox.com的任何子域(如p2p.wrox.com)都有效。

secure标志是cookie中唯一一个非名值对儿的部分,直接包含一个secure单词。如下:

1
2
3
4
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
Other-header: other-header-value

这里,创建了一个对于所有wrox.com的子域和域名下(由path参数指定的)所有页面都有效的cookie。因为设置了secure标志,这个cookie只能通过SSL连接才能传输。
尤其要注意,域、路径、失效时间和secure标志都是服务器给浏览器的指示,以指定何时应该发送cookie。这些参数并不会作为发送到服务器的cookie信息的一部分,只有名值对儿才会被发送。

3. JavaScript中的cookie

在JavaScript中处理cookie有些复杂,因为其众所周知的蹩脚的接口,即BOM的document.cookie属性。这个属性的独特之处在于它会因为使用它的方式不同而表现出不同的行为。当用来获取属性值时,document.cookie返回当前页面可用的(根据cookie的域、路径、失效时间和安全设置)所有cookie的字符串,一系列由乡间-隔开的名值对儿,如下例所示。

1
name1=value1;name2=value2;name3=value3;

所有名字和值都是经过URL编码的,所以必须使用decodeURIComponent()来解码。
当用于设置值的时候,document.cookie属性可以设置为一个新的cookie字符串。这个cookie字符串会被解释并添加到现有的cookie集合中。设置document.cookie并不会覆盖cookie,除非设置的cookie的名称已经存在。设置cookie的格式如下,和Set-Cookie头中使用的格式-祥。

1
name=value;expires=expiration_time;path=domain_path;domain=domain_name;secure

这些参数中,只有cookie的名字和值是必需的。下面是一个简单的例子。

1
document.cookie = 'name=Micheal';

这段代码创建了一个叫name的cookie,值为Nicholas。当客户端每次向服务器端发送请求的时候,都会发送这个cookie;当浏览器关闭的时候,它就会被删除。虽然这段代码没问题,但因为这里正好名称和值都无需编码,所以最好每次设置cookie时都像下面这个例子中一样使用encodeURIComponent()。

1
document.cookie = encodeURIComponent('name') + '=' + encodeURIComponent('Micheal');

要给被创建的cookie指定额外的信息,只要将参数追加到该字符串,和Set-Cookie头中的格式一样,如下所示。

1
document.cookie = encodeURIComponent('name') + '=' + encodeURIComponent('Micheal') + '; domain=.michealwayne.cn; path=/';

由于JavaScript中读写cookie不是非常直观,常常需要写一些函数来简化cookie的功能。基本的cookie操作有三种:读取、写入和删除。它们在cookieUtil对象中如下表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var CookieUtil = {
get: function (name) {
var cookieName = encodeURIComponent(name) + '=',
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = null;

if (cookieStart > -1) {
var cookieEnd = document.cookie.indexOf(';', cookieStart);
if (cookieEnd === -1) cookieEnd = document.cookie.length;
cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd));
}

return cookieValue;
},

set: function (name, value, expires, path, domain, secure) {
var cookieText = encodeURIComponent(name) + '=' + encodeURIComponent(value);

if (expires instanceof Date) {
cookieText += '; expires=' expires.toGMTString();
}

if (path) {
cookieText += '; path=' + path;
}

if (domain) {
cookieText += '; domain=' + domain;
}

if (secure) {
cookieText += '; secure';
}

document.cookie = cookieText;
},

unset: function (name, path, domain, secure) {
this.set(name, '', new Date(0), path, domain, secure);
}
};

CookieUtil.get()方法根据cookie的名字获取相应的值。它会在document.cookie字符串中查找cookie名加上等于号的位置。如果找到了,那么使用indexOf()查找该位置之后的第一个分号(表示了该cookie的结束位置)。 如果没有找到分号,则表示该cookie是字符串中的最后一个,则余下的字符串都是cookie的值。该值使用decodeURIComponent()进行解码并最后返回。如果没有发现cookie,则返回null。
CookieUtil.set()方法在页面上设置一个cookie,接收如下几个参数:cookie的名称,cookie的值,可选的用于指定cookie何时应被删除的Date对象,cookie的可选的URL路径,可选的域,以及可选的表示是否要添加secure标志的布尔值。参数是按照它们的使用频率排列的,只有头两个是必需的。在这个方法中,名称和值都使用encodeURIComponent()进行了URL编码,并检查其他选项。如果expires 参数是Date对象,那么会使用Date对象的toGMTString()方法正确格式化Date对象,并添加到expires选项上。方法的其他部分就是构造cookie字符申并将其设置到document.cookie中。
没有删除己有cookie的直接方法。所以,需要使用相同的路径、域和安全选项再次设置cookie,并将失效时间设置为过去的时间。CookieUtil.unset()方法可以处理这种事情。它接收4个参数:要删除的cookie的名称、可选的路径参数、可选的域参数和可选的安全参数。
这些参数加上空字符串并设置失效时间为1970年1月1日(初始化为0ms的Date对象的值).传给CookieUtil.set()。 这样就能确保删除cookie。

4.子cookie

为了绕开浏览器的单域名下的cookie数限制,一些开发人员使用了一种称为子cookie(subcookie)的概念。 子cookie是存放在单个cookie中的更小段的数据。也就是使用cookie值来存储多个名称值对儿。子cookie最常见的的格式如下所示。

1
name=name1=value1&name2=value2&name3=value3

子cookie一般也以查询字符串的格式进行格式化。然后这些值可以使用单个cookie进行存储和访问,而非对每个名称-值对儿使用不同的cookie存储。最后网站或者Web应用程序可以无需达到单域名cookie上限也可以存储更加结构化的数据。
为了更好地操作子cookie,必须建立一系列新方法。子cookie的解析和序列化会因子cookie的期望用途而略有不同并更加复杂些。例如,要获得一个子cookie,首先要遵循与获得cookie一样的基本步骤.但是在解码cookie值之前,需要按如下方法找出子cookie的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var SubCookieUtil = {
get: function (name, subName) {
var subCookies = this.getAll(name);
if (subCookies) {
return subCookies(subName);
} else return null;
},

getAll: function (name) {
var cookieName = encodeURIComponent(name) + '=',
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = null,
cookieEnd,
subCookies,
i,
parts,
result = {};

if (cookieStart > -1) {
cookieEnd = document.cookie.indexOf(';', cookieStart);
if (cookieEnd > -1) {
cookieEnd = document.cookie.length;
}
cookieValue = document.cookie.substring(cookieStart + cookieName.length, cookieEnd);

if (cookieValue.length > -1) {
subCookies = cookieValue.split('&');
for (i = 0, len = subCookies.length; i < len; i++) {
parts = subCookies[i].split('&');
result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
}

return result
}
}

return null;
}
}

获取子cookie的方法有两个:get()和getAll()。其中get()获取单个子cookie的值,getAll()在取所有子cookie并将它们放入一个对象中返回,对象的属性为子cookie的名称,对应值为子cookie对应的值。get()方法接收两个参数:cookie的名字和子cookie的名字。它其实就是调用getAll()获取所有的子cookie,然后只返回所需的那一个(如果cookie不存在则返回null)。

SubCookieUtil.getAll()方法和CookieUtil.get()在解析cookie值的方式上非常相似。区别在于cookie的值并非立即解码,而是先根据&字符将于cookie分割出来放在一个数组中,每一个子cookie再根据等于号分割,这样在parts数组中的前一部分便是子cookie名,后一部分则是子cookie的值。 这两个项目都要使用decodeURIComponent()来解码,然后放入result对象中,最后作为方法的返回值。如果cookie不存在,则返回null。

要设置子cookie,也有两种方法:set()和setAll()。以下代码展示了它们的构造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
var SubCookieUtil = {

get: function (name, subName){
var subCookies = this.getAll(name);
if (subCookies){
return subCookies[subName];
} else {
return null;
}
},

getAll: function(name){
var cookieName = encodeURIComponent(name) + "=",
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = null,
cookieEnd,
subCookies,
i,
parts,
result = {};

if (cookieStart > -1){
cookieEnd = document.cookie.indexOf(";", cookieStart)
if (cookieEnd == -1){
cookieEnd = document.cookie.length;
}
cookieValue = document.cookie.substring(cookieStart + cookieName.length, cookieEnd);

if (cookieValue.length > 0){
subCookies = cookieValue.split("&");

for (i=0, len=subCookies.length; i < len; i++){
parts = subCookies[i].split("=");
result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
}

return result;
}
}

return null;
},

set: function (name, subName, value, expires, path, domain, secure) {

var subcookies = this.getAll(name) || {};
subcookies[subName] = value;
this.setAll(name, subcookies, expires, path, domain, secure);

},

setAll: function(name, subcookies, expires, path, domain, secure){

var cookieText = encodeURIComponent(name) + "=",
subcookieParts = new Array(),
subName;

for (subName in subcookies){
if (subName.length > 0 && subcookies.hasOwnProperty(subName)){
subcookieParts.push(encodeURIComponent(subName) + "=" + encodeURIComponent(subcookies[subName]));
}
}

if (subcookieParts.length > 0){
cookieText += subcookieParts.join("&");

if (expires instanceof Date) {
cookieText += "; expires=" + expires.toGMTString();
}

if (path) {
cookieText += "; path=" + path;
}

if (domain) {
cookieText += "; domain=" + domain;
}

if (secure) {
cookieText += "; secure";
}
} else {
cookieText += "; expires=" + (new Date(0)).toGMTString();
}

document.cookie = cookieText;

},

unset: function (name, subName, path, domain, secure){
var subcookies = this.getAll(name);
if (subcookies){
delete subcookies[subName];
this.setAll(name, subcookies, null, path, domain, secure);
}
},

unsetAll: function(name, path, domain, secure){
this.setAll(name, null, new Date(0), path, domain, secure);
}

};

5.关于cookie的思考

还有一类cookie被称为“HTTP专有cookie”。HTTP专有cookie可以从浏览器或者服务器设置,但是只能从服务器端读取,因为JavaScript无法获取HTTP专有cookie的值。
由于所有的cookie都会自浏览器作为请求头发送。所以在cookie中存储大量信息会影响到特定域的请求性能。cookie信息越大,完成对服务器请求的时间也就越长。尽管浏览椿对cookie进行了大小限制,不过最好还是尽可能在cookie中少存储信息,以避免影响性能。
cookie的性质和它的局限使得其并不能作为在储大量信息的理想手段,所以又出现了其他方法。

一定不要在cookie中存储重要和敏感的数据。cookie数据并非存储在一个安全环境中,其中包含的任何数据都可以被他人访问。所以不要在cookie中存储诸如信用卡号或者个人地址之类的极据。

* 3.2 IE用户数据

不作介绍

3.3 Web 存储机制

Web Storage最早是在Web超文本应用技术工作组(WHAT-WG)的Web应用1.0规范中描述的。 这个规范的最初的工作最终成为了HTML5的一部分。WebStorage的目的是克服自cookie带来的一些限制,当数据需要被严格控制在客户端上时,无须持续地将数据发回服务器。Web Storage的两个主要目标是:

  • 提供一种在cookie之外存储会话数据的途径:
  • 提供一种存储大量可以跨会话存在的数据的机制。

最初的Web Storage规范包含了两种对象的定义:sessionStorage和globalStorage。这两个对象在支持的浏览器中都是以windows对象属性的形式存在的,支持这两个属性的浏览器包括IE8+、Firefox 3.5+、 Chrome4+和 Opera 10.5+。

1.storage类型

Storage类型提供最大的存储空间(因浏览器而异)来存储名值对儿。Storage的实例与其他对象类似,有如下方法。

  • clear(): 删除所有值;Firefox中没有实现。
  • getItem(name):根据指定的名字name获取对应的值 。
  • key(index):获得index位置处的值的名字。
  • removeitem(name):删除由name指定的名值对儿。
  • setitem(name, value):为指定的name 设置一个对应的值 。

其中,getItem()、removeItem()和setItem()方法可以直接调用,也可通过Storage对象间接调用。因为每个项目都是作为属性存储在该对象上的.所以可以通过点语法或者方括号语法访问属性来读取值,设置也一样,或者通过delete操作符进行删除。不过,我们还建议读者使用方法而不是属性来访问数据,以免某个键会意外重写该对象上已经存在的成员。
还可以使用length属性来判断有多少名值对存放在Storage对象中。但无法判断对象中所有数据的大小,不过IE8提供了一个remainingSpace属性,用于获取还可以使用的存储空间的字节数。

Storage类型只能存储字符串。非字符串的数据在存储之常会被转换成字符串。

2. sessionStorage对象

sessionStorage对象存储特定于某个会话的数据,也就是该数据只保持到浏览器关闭。这个对象就像会话cookie,也会在浏览器关闭后消失。存储在sessionStorage中的数据可以跨越页面刷新而存在,同时如果浏览器支持,浏览器崩溃并重启之后依然可用(Firefox和WebKit都支待,IE则不行)。
因为seesionStorage对象绑定于某个服务器会话,所以当文件在本地运行的时候是不可用的。存储在sessionStorage中的数据只能由最初给对象存储数据的页面访问到,所以对多页面应用有限制。
由于sessionStorage对象其实是Storage的一个实例,所以可以使用setItem()或者直接设置新的属性来存储数据。下面是这两种方法的例子。

1
2
3
4
// 使用方法存储数据
sessionStorage.setItem('name', 'Micheal');
// 使用属性存储数据
sessionStorage.book = 'Js';

不同浏览器写入数据方面略有不同。Firefox和WebKit实现了同步写入,所以添加到存储空间中的数据是立刻被提交的。而IE的实现则是异步写入数据,所以在设置数据和将数据实际写入磁盘之间可能有一些延迟。对于少量数据而言,这个差异是可以忽略的。对于大量数据,你会发现IE要比其他浏览器更快地恢复执行,因为它会跳过实际的磁盘写入过程。
在IE8中可以强制把数据写入磁盘:在设置新数据之前使用begin()方法,并且在所有设置完成之后调用commit()方法。看以下例子。

1
2
3
4
5
// 只适用于IE8
sessionStorage.begin();
sessionStorage.name = 'Micheal';
sessionStorage.book = 'js';
sessionStorage.commit();

这段代码确保了name和book的值在调用commit()之后立刻被写入磁盘。调用begin()是为了确保在这段代码执行的时候不会发生其他磁盘写入操作。对于少量数据而言,这个过程不是必需的;不过,对于大量数据(如文档之类的)可能就要考虑这种事务形式的方法了。
sessionStorage中有数据时,可以使用getItem()或者通过直接访问属性名来获取数据。两种方法的例子如下。

1
2
3
4
5
// 使用方法读取数据
var name = sessionStorage.getItem('name');

// 使用属性读取数据
var book = sessionStorage.book;

还可以通过结合length属性和key()方法来迭代sessionStorage中的值,如

1
2
3
4
5
for (var i = 0, len = sessionStorage.length; i < len; i++) {
var key = sessionStorage.key(i);
var value = sessionStorage.getItem(key);
alert(key + '=' + value);
}

还可以使用for-in循环来迭代sessionStorage中的值:

1
2
3
4
for (var key in sessionStorage) {
var value = sessionStorage.getItem(key);
alert(key + '=' + value);
}

每次经过循环的时候,key被设置为sessionStorage中下一个名字,此时不会返回任何内置方法或length属性。
要从sessionStorage中删除数据,可以使用delete操作符删除对象属性,也可调用removeItem()方法。以下是这些方法的例子。

1
2
3
4
5
// 使用delete(webkit无效)
delete sessionStorage.name;

// 使用方法
sessionStorage.removeItem('name');

sessionStorage对象应该主要用于仅针对会话的小段数据的存储。如果需要跨越会话存储数据,那么globalStorage或者localStorage更为合适。

*3.globalStorage对象(兼容目测仅火狐)

Firefox 2中实现了globalStorage对象。作为最初的WebStorage规范的一部分,这个对象的目的是跨越会话存储数据,但有特定的访问限制。要使用globalStorage,首先要指定哪些域可以访问该数据。可以通过方括号标记使用属性米实现,如

1
2
3
4
5
// 保存数据
globalStorage['wrox.com'].name = 'Micheal';

// 获取数据
console.log(globalStorage['wrox.com'].name)

在这里,访问的是针对域名wrox.com的存储空间。globalStorage对象不是Storage的实例,而具体的globalStorage[‘wrox.com’]才是。这个存储空间对于wrox.com及其所有子域都是可以访问的。可以像下面这样指定子域名。

1
2
3
4
5
// 保存数据
globalStorage['www.wrox.com'].name = 'Micheal';

// 获取数据
console.log(globalStorage['www.wrox.com'].name)

某些浏览器允许更加宽泛的访问限制,比如只根据顶级域名进行限制或者允许全局访问,如下面例子所示。

1
2
3
4
5
// 保存数据
globalStorage[''].name = 'Micheal';

// 保存数据,.net结尾的域名都可访问
globalStorage['net'].name = 'Micheal';

虽然这些也支持,但是还是要避免使用这种可宽泛访问的数据存储,以防止出现潜在的安全问题。 考虑到安全问题,这些功能在未来可能会被删除或者是被更严格地限制,所以不应依赖于这类功能。当使用globalStorage的时候-定要指定一个域名。
对globalStorage空间的访问,是依据发起请求的页丽的域名、协议和端口来限制的。例如,如果使用HITPS协议在wrox.com中存储了数据,那么通过HTIP访问的wrox.com的页商就不能访问该数据。同样,通过80端口访问的页面则无法与同一个域同样协议但通过8080端口访问的页面共享数据。这类似于Ajax请求的同源策略。

如果不使用removeItem()或者delete删除,或者用户未清除浏览器缓存,存储在 globalStorage属性中的数据会一直保留在磁盘上。这让globalStorage非常适合在客户端存储文皑或者长期保存用户偏好设置。

4.localStorage对象

localStorage对象在修订过的HTML5规范中作为持久保存客户端数据的方案取代了globalStorage。与globalStorage不同,不能给localStorage指定任何访问规则;规则事先就设定好了。要访问同一个localStorage对象,页面必须来自同一个域名(子域名无效),使用同一种协议,在同一个端口上。这相当于globalStorage[location.host]。
由于localStorage是Storage的实例,所以可以像使用sessionStorage一样来使用。下面是一些例子。

1
2
3
4
5
6
7
8
9
10
11
// 使用方法存储数据
localStorage.setItem('name', 'Micheal');

// 使用属性存储数据
localStorage.book = 'js';

// 使用方法读取数据
var name = localStorage.getItem('name');

// 使用属性读取数据
var book = localStorage.book;

存储在localStorage中的数据和存储在globalStorage中的数据-样,都遵循相同的规则:数据保留到通过JavaScript删除或者是用户消除浏览器缓存。

为了兼容只支持globalStorage的浏览楞,可以使用以下函数。

1
2
3
4
5
6
7
function getLocalStorage () {
if (typeof localStorage === 'object') {
return localStorage;
} else if (typeof globalStorage === 'object') {
return globalStorage;
} else throw new Error('Local storage not available');
}

5.storage事件

对Storage对象进行任何修改,都会在文档上触发storage事件。当通过属性或setItem()方法保存数据,使用delete操作符或removeitem()删除数据,或者调用clear()方法时,都会发生该事件。这个事件的event对象有以下属性。

  • domain:发生变化的存储空间的域名。
  • key:设置或者删除的键名。
  • newValue:如果是设置值,则是新值;如果是删除键,则是null。
  • oldValue:键被更改之前的值。

在这四个属性中,IE8和Firefox只实现了domain属性。如

1
2
3
EventUtil.addHandler(document, 'storage', function (event) {
alert('Storage changed for ' + event.domain)
})

无论对sessionStorage、globalStorage还是localStorage进行操作,都会触发storage事件,但不作区分。

6.限制

与其他客户端数据存储方案类似,Web Storage同样也有限制。这些限制因浏览器而异。一般来说,对存储空间大小的限制都是以每个来源(协议、域和端口)为单位的。换句话说,每个来源都有固定大小的空间用于保存自己的数据。考虑到这个限制,就要注意分析和控制每个来源中有多少页面需要保存数据。

  • 对于localStorage而言,大多数桌面浏览器会设置每个来源5MB的限制。Chrome和Safari对每个来源的限制是2.5MB。而iOS版Safari和Android版WebKit的限制也是2.5MB。
    – 对sessionStorage的限制也是因浏览器而异。有的浏览器对sessionstorage的大小没有限制,但Chrome、Safari、iOS版Safari和Android版WebKit都有限制,也都是2.5M。IE8+和Opera对sessionStorage的限制是5MB。**

3.4 IndexedDB

Indexed Database API,或者简称为IndexedDB,是在浏览器中保存结构化数据的一种数据库。IndexedDB是为了替代目前已被废弃的WebSQL Database API而出现的。IndexedDB的思想是创建一套API,方便保存和读取JavaScript对象,同时还支持查询及搜索。
IndexedDB设计的操作完全是异步进行的。因此,大多数操作会以请求方式进行,但这些操作会在后期执行,然后如果成功则返回结果,如果失败则返回错误。差不多每一次IndexedDB操作,都需要你注册onerror或onsuccess事件处理程序,以确保适当地处理结果。
在得到完整支持的情况下,IndexedDB将是一个作为API宿主的全局对象。由于API仍然可能有变化,浏览器也都使用提供商前缀,因此这个对象在IE10中叫msIndexedDB,在Firefox4中叫mozIndexedDB,在Chrome中叫webkitIndexedDB。为了清楚起见,本节示例中将使用IndexedDB,而实际土每个示例前面都应该加上下面这行代码:

1
var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB;

1.数据库

IndexedDB就是一个数据库,与MySQL或WebSQL Database等这些你以前可能用过的数据库类似。IndexedDB最大的特色是使用对象保存数据,而不是使用表来保存数据。一个IndexedDB数据库,就是一组位于相同命名空间下的对象的集合。
使用IndexedDB的第一步是打开它,即要打开的数据库名传给indexDB.open()。如果传入的数据库已经存在,就会发送一个打开它的请求;如果传入的数据库还不存在,就会发送一个创建并打开它的请求。总之,调用indexDB.open()会返回一个IDBRequest对象,在这个对象上可以添加onerror和onsuccess事件处理程序。先来看个例子

1
2
3
4
5
6
7
8
9
10
11
var request,
database;

request = indexedDB.open('admin');

request.onerror = function (event) {
alert('Something bad happened while trying to open:' + event.target.errorCode);
};
request.onsuccess = function (event) {
database = event.target.result;
};

在这两个事件处理程序中,event.target都指向request对象,因此它们可以互换使用。如果响应的是onsuccess事件处理程序,那么event.target.result中将有一个数据库实例对象。(IDBDatabase),这个对象会保存在database变量中。如果发生了错误,那event.target.errorCode中将保存一个错误码,表示问题的性质。一下就是可能的错误码(这个错误码适合所有操作)。

  • IDBDatabaseException.UNKNOW_ERR(1):意外错误,无法归类。
  • IDBDatabaseException.NON_TRANSIENT_ERR(2):操作不合法。
  • IDBDatabaseException.NO_FOUND_ERR(3):未发现要操作的数据库。
  • IDBDatabaseException.CONSTRAINT_ERR(4):违反了数据库约束。
  • IDBDatabaseException.DATA_ERR(5):提供给事务的数据不能满足要求。
  • IDBDatabaseException.NOT_ALLOWED_ERR(6):操作不合法。
  • IDBDatabaseException.TRANSACTION_INACTIVE_ERR(7):试图重用已完成的事务。
  • IDBDatabaseException.ABORT_ERR(8):请求中断,未成功。
  • IDBDatabaseException.READ_ONLY_ERR(9):试图在只读模式下写入或修改数据。
  • IDBDatabaseException.TIMEOUT_ERR(10):在有效时间内未完成操作。
  • IDBDatabaseException.QUOTA_ERR(11):磁盘空间不足。

默认情况下,lndexedDB数据库是没有版本号的,最好一开始就为数据库指定一个版本号。为此,可以调用setVersion()方法,传入以字符串形式表示的版本号。同样,调用这个方法也会返回一个请求对象,需要你再指定事件处理程序。

1
2
3
4
5
6
7
8
9
10
11
if (database.version != '1.0') {
request = database.setVersion('1.0');
request.onerror = function (event) {
alert('Something bad happened whil trying to set version:' + event.target.errorCode);
};
request.onsuccess = function (event) {
alert('Database initialization complete. Database name:' + database.name + ', Version:' + database.version);
};
} else {
alert('Database already initialized.Database name :' + database.name + ', Version: ' + database.version);
}

这个例子尝试把数据库的版本号设置为1.0。第一行先检测version属性,看是否已经为数据库设置了相应的版本号。如果没有,就调用setVersion()创建修改版本的请求。如果请求成功,显示一条消息,表示版本修改成功。
如果数据库的版本号已经被设置为1.0,则显示一条消息,说明数据库已经初始化过了。总之,通过这种模式,就能知道你想使用的数据库是否已经设置了适当的对象存储空间。在整个Web应用中,随着对数据库结构的更新和修改,可能会产生很多个不同版本的数据库。

2.对象存储空间

在建立了与数据库的连接之后,下一步就是使用对象存储空间飞如果数据库的版本与你传入的版本不匹配,那可能就需要创建一个新的对象存储空间。在创建对象存储空间之前,必须要想清楚你想要 保存什么数据类型。
假设你要保存的用户记录自用户名、密码等组成,那么保存一条记录的对象应该类似如下所示:

1
2
3
4
5
6
var user = {
usrname: '007',
firstName: 'James',
lastName: 'Bond',
password: 'foo'
};

有了这个对象,很容易想到username属性可以作为这个对象存储空间的键。这个username必须全局唯一,而且大多数时候都要通过这个键采访问数据。这一点非常重要,因为在创建对象存储空间时,必须指定这么一个键。以下是就是为保存上述用户记录而创建对象存储空间的示例。

1
var store = db.createObjectStore('users', { keyPath: 'username' });

其中第二个参数中的keyPath属性,就是空间中将要保存的对象的一个属性,而这个属性将作为 存储空间的键来使用。
好,现在有了一个对存储空间的引用。接下来可以使用add()或put()方法来向其中添加数据。这两个方法都接收一个参数,即要保存的对象,然后这个对象就会被保存到存储空间中。这两个方法的区别在空间中已经包含键值相同的对象时会体现出来。在这种情况下,add()会返回错误,而put()则会重写原有对象。简单地说,可以把add()想象成插入新值,把put()想象成更新原有的值。在初始化对象存储空间时,可以使用类似下面这样的代码。

1
2
3
4
5
6
7
// users中保存着一批用户对象
var i = 0,
len = users.length;

while(i < len) {
store.add(users[i++]);
}

每次调用add()或put()都会创建一个新的针对这个对象存储空间的更新请求。如果想验证请求是否成功完成,可以把返回的请求对象保存在-个变量中.然后再指定onerror或onsuccess事件处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// users中保存着一批用户对象
var i = 0,
request,
requests = [],
len = user.length;

while (i < len) {
request = store.add(users[i++]);
request.onerror = function () {
// error
};
request.onsuccess = function () {
// success
};
request.push(request);
}

创建了对象存储空间并向其中添加了数据之后,就该查询数据了。

3.事务

跨过创建对象存储空间这一步之后,接下来的所有操作都是通过事务来完成的。在数据库对象上调用transaction()方法可以创建事务。任何时候,只要想读取或修改数据,都要通过事务来组织所有操作。在最简单的情况下,可以像下面这样创建事务。

1
var transaction = db.transaction();

如果没有参数,就只能通过事务来读取数据库中保存的对象。是常见的方式是传入要访问的一或多个对象存储空间。

1
var transaction = db.transaction('users');

这样就能保证只加载users存储空间中的数据,以便通过事务进行防问。 如柴要访问多个对象存储空间,也可以在第一个参数的位置上传入字符串数组。

1
var transaction = db.transaction(['users', 'anotherStore]);

如前所述,这些事务都是以只读方式访问数据。要修改访问方式,必须在创建事务时传入第二个参数,这个参数表示访问模式,用IDBTransaction接口定义的如下常量表示:READ_ONLY(0)表示只读;READ_WRITE(1)表示读写,VERSION_CHANGE(2)表示改变。IE10+和Firefox4+实现的是IDBTransaction,但在Chrome中则叫webkitIDBTransaction,所以使用下面的代码可以统一接口:

1
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;

有了这行代码,就可以更方便地为transaction()指定第二个参数了。

1
var transaction = db.transaction('user', IDBTransaction.READ_WRITE);

这个事务能够读写users存储空间。
取得了事务的索引后,使用objectstore()方法并传入存储空间的名称,就可以访问特定的存储空间。然后,可以像以前一样使用add()和put()方法,使用get()可以取得值,使用delete()可以删除对象,而使用clear()则可以删除所有对象。get()和delete()方法都接收一个对象键作为参数,而所有这5个方法都会返回一个新的请求对象。例如:

1
2
3
4
5
6
7
8
9
var request = db.transaction('user').objectStore('users').get('007');
request.onerror = function (event) {
alert('Did not get the object');
};

request.onsuccess = function (event) {
var result = event.target.result;
alert(result.firstName);
};

因为一个事务可以完成任何多个请求, 所以事务对象本身也有察件处理程序: onerror和oncomplete。这两个事件可以提供事务级的状态信息。

1
2
3
4
5
6
7
transaction.onerror = function(event) {

};

transaction.oncomplete = function(event) {

};

注意,通过oncomplete事件的事件对象(event)访问不到get()请求返回的任何数据。必须在相应请求的onsuccess事件处理程序中才能访问到数据。

4.使用游标查询

使用事务可以直接通过已知的键检索单个对象。而在需要检索多个对象的情况下,则需要在事务内部创建游标。游标就是-指向结果集的指针。与传统数据库查询不同,游标并不提前收集结果。游标指针会先指向结果中的第…项,在接到查找下一项的指令时,才会指向下一项。
在对象存储空间上调用openCursor()方法可以创建游标。与IndexedDB中的其他操作一样,openCursor()方法返回的是一个请求对象,因此必须为该对象指定onsuccess和onerror事件处理 程序。例如:

1
2
3
4
5
6
7
8
9
var store = db.transaction('users').objectStore('users'),
request = store.openCursor();

request.onsuccess = function (event) {
// success
};
request.onerror = function (event) {
// error
};

在onsuccess事件处理程序执行时,可以通过event.target.result取得存储空间中的下一个对象。在结果集中有下-项时,这个属性中保存一个IDBCursor的实例,在没有下一项时,这个属性 的值为null。IDBCursor的实例有以下几个属性。

  • direction:数值,表示游标移动的方向。默认值为IDBCursor.NEXT(0),表示下-项。IDBCursor.NEXT_NO_DUPLICATE(1)表示下一个不重复的项,IDBCursor.PREV(2)表示前一项,而IDBCursor.PREV_NO_DUPLICATE表示前一个不重复的项。
  • key:对象的键。
  • value:实际的对象。
  • primaryKey:游标使用的键。可能是对象键,也可能是索引键(稍后讨论索引键)。
    要检索某一个结果的信息,可以像下面这样:
    1
    2
    3
    4
    5
    6
    request.onsuccess = function (event) {
    var cursor = event.target.result;
    if (cursor) {
    console.log(cursor.key, JSON.stringify(cursor.value));
    }
    }

请记住,这个例子中的cursor.value是一个对象,这也是为什么在显示它之前先将它转换成JSON字符串的原因。
使用游标可以更新个别的记录。调用update()方程可以用指定的对象更新当前游标的value。与其他操作一样,调用update()方法也会创建一个新请求,因此如果你想知道结果,就要为它指定onsuccess和onerror事件处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
request.onsuccess = function (event) {
var cursor = event.target.result,
value,
updateRequest;

if (cursor) {
if (cursor.key == 'foo') {
value = cursor.value;
value.password = 'magic!';

updateRequest = cursor.update(value);
updateRequest.onsuccess = function () {
// success
};
updateRequest.onerror = function () {
// error
}
}
}
}

此时,如果调用delete()方法,就会删除相应的记录。与update() 一样,调用delete()也返回一个请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
request.onsuccess = function (event) {
var cursor = event.target.result,
value,
deleteRequest;

if (cursor) {
if (cursor.key == 'foo') {

deleteRequest = cursor.delete();
deleteRequest.onsuccess = function () {
// success
};
deleteRequest.onerror = function () {
// error
}
}
}
}

如果当前事务没有修改对象存储空间的权限,update()和delete()会抛出错误。
默认情况下,每个游标只发起一次请求。要想发起另一次请求,必须调用下面的一个方法。

  • continue(key):移动到结果集中的下一项。参数key是可选的,不指定这个参数,游标移动到下一项;指定这个参数,游标会移动到指定键的位置。
  • advance(count):向前移动count指定的项数。

这两个方法都会导致游标使用相同的请求,因此相同的onsuccess和onerror事件处理程序也会得到重用。例如,下面的例子遍历了对象存储空间中的所有项。

1
2
3
4
5
6
7
8
9
10
request.onsuccess = function (event) {
var cursor = event.target.result,
value,
deleteRequest;

if (cursor) {
console.log(cursor.key, JSON.stringify(cursor.value));
cursor.continue();
} else console.log('Done')
}

调用continue()会触发另一次请求,进而再次调用onsuccess事件处理程序。在没有更多项可以迭代时,将最后一次调用onsuccess事件处理程序,此时event.target.result的值为null。

5.键范围

使用游标总让人觉得不那么理想,因为通过游标查找数据的方式太有限了。键范围( key range )为使用游标增添了一些灵活性。键范围由IDBKeyRange的实例表示。支持标准 IDBKeyRange类型的浏览器有IE10+和Firefox 4+,Chrome中的名字叫webkitIDBKeyRange。与使用IndexedDB中的其他类型一样,你最好先声明一个本地的类型,同时要考虑到不同浏览器中的差异。

1
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

有四种定义键范围的方式。第一种是使用only()方法,传入你想要取得的对象的键。

1
var onlyRange = IDBKeyRange.only('007');

这个班圈可以保证只取得键为”007”的对象。使用这个范围创建的游标与直接访问存储空间并调用get(‘007’)差不多。

第二种定义键范围的方式是指定结果集的下界。下界表示游标开始的位置。例如,以下键范围可以保证游标从键为’007’的对象开始,然后继续向前移动,直至最后一个对象。

1
var lowerRange = IDBKeyRange.lowerBound('007');

如果你想忽略键为”007”的对象,从它的下一个对象开始,那么可以传入第二个参数true:

1
var lowerRange = IDBKeyRange.lowerBound('007', true);

第三种定义键范围的方式是指定结果集的上界,也就是指定游标不能超越哪个键。指定上界使用upperRange()方法。下面这个键范围可以保证游标从头开始,到取得键为’ace’的对象终止。

1
var upperRange = IDBKeyRange.upperRange('ace');

如果你不想包含键为指定值的对象,同样,传入第二个参数 true:

1
var upperRange = IDBKeyRange.upperRange('ace', true);

第四种定义键范围的方式——没错,就是同时指定上、下界,使用bound()方法。这个方法可以接收4个参数:表示下界的键、表示上界的键、可选的表示是否跳过下界的布尔值和可选的表示是否跳过 上界的布尔值。以下是几个例子。

1
2
3
4
5
var boundRange = IDBKeyRange.bound('007', 'ace');

var boundRange = IDBKeyRange.bound('007', 'ace', true);

var boundRange = IDBKeyRange.bound('007', 'ace', true, true);

无论如何,在定义键施围之后,把它传给openCursor()方法,就能得到一个符合相应约束条件的游标。

1
2
3
4
5
6
7
8
9
10
11
var store = db.transaction('users').objectStore('users'),
range = IDBKeyRange.bound('007', 'ace'),
request = store.openCursor(range);

request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
console.log(cursor.key, JSON.stringify(cursor.value));
cursor.continue();
} else console.log('Done');
}

这个例子输出的对象的键为”007”到”ace”,比上一节最后那个例子输出的值少一些。

6.设定游标方向

实际上,openCursor()可以接收两个参数。第一个参数就是刚刚看到的IDBKeyRange的实例,第二个是表示方向的数值常量。作为第二个参数的常量是前面讲查询时介绍的IDBCursor中的常量。Firefox4+和Chrome的实现又有不同,因此第一步还是在本地消除差异:

1
var IDBCursor = window.IDBCursor || window.webkitIDBCursor;

正常情况下,游标都是从存储空间的第一项开始,调用 continue()或advance ()前进到最后-项。游标的默认方向值是IDBCursor.NEXT。如果对象存储空间中有重复的项,而你想让游标跳过那些重复的项,可以为openCursor传入IDBCursor.NEXT_NO_DUPLICATE作为第二个参数:

1
2
var store = db.transaction('users').objectStore('users'),
request = store.openCursor(null, IDBCursor.NEXT_NO_DUPLICATE);

注意,openCursor()的第一个参数是null,表示使用默认的键范围,即包含所有对象。这个游标可以从存储空间中的第一个对象开始,逐个选代到最后一个对象——但会跳过重复的对象。
当然,也可以创建一个游标,让它在对象存储空间中向后移动,即从最后一个对象开始,逐个迭代.直至第一个对象。此时,要传入的常量是IDBCursor.PREV和IDBCursor.PREV_NO_DUPLICATE。例如:

1
2
var store = db.transaction('users').objectStore('users'),
request = store.openCursor(null, IDBCursor.PREV);

使用IDBCursor.PREV或IDBCursor.PREV_NO_DUPLICATE打开游标时,每次调用continue()或advance(),都会在存储空间中向后而不是向前移动游标。

7.索引

对于某些数据,可能需要为一个对象存储空间指定多个键。比如,若要通过用户ID和用户名两种方式来保存用户资料,就需要通过这两个键来存取记录。为此,可以考虑将用户ID作为主键,然后为用户名创建索引。
要创建索引,首先引用对象存储空间,然后调用createIndex()方法,如下所示。

1
2
var store = db.transaction('users').objectStore('users'),
index = store/createIndex('username', 'username', { unique: false })

createIndex()的第-个参数是索引的名字,第二个参数是索引的属性的名字,第三个参数是一个包含unique属性的选项(options)对象。这个选项通常都必须指定,因为它表示键在所有记录中是否唯一。因为username有可能重复,所以这个索引不是唯一的。
createIndex()的返回值是IDBIndex的实例。在对象存储空间上调用index()方法也能返回同一个实例。例如,要使用一个已经存在的名为”username“的索引,可以像下面这样取得该索引。

1
2
var store = db.transaction('users').objectStore('users'),
index = store.index('username')

索引其实与对象存储空间很相似。在索引上调用opencursor()方法也可以创建新的游标,除了将来会把索引键而非主键保存在event.result.key属性中之外,这个游标与在对象存储空间上调用openCursor()返回的游标完全一样。来看下面的例子。

1
2
3
4
5
6
7
var store = db.transaction('users').objectStore('users'),
index = store.index('username'),
request = index.openCursor();

request.onsuccess = function (event) {
// success
};

在索引上也能创建一个特殊的只返回每条记录主键的游标,那就要调用openKeyCursor()方法。这个方法接收的参数与openCursor()相同。 而最大的不同在于这种情况下event.result.key中仍然保存着索引键,而event.result.value中保存的则是主键,而不再是整个对象。

1
2
3
4
5
6
7
8
var store = db.transaction('users').objectStore('users'),
index = store.index('username'),
request = index.openCursor();

request.onsuccess = function (event) {
// success
// event.result.key中保存索引键,而event.result.value中保存主键
};

同样,使用get()方法能够从索引中取得一个对象,只要传入相应的索引键即可;当然,这个方法也将返回一个请求。

1
2
3
4
5
6
7
8
9
10
11
12
var store = db.transaction('users').objectStore('users'),
index = store.index('username'),
request = index.get('007');

request.onsuccess = function (event) {
// success
// event.result.key中保存索引键,而event.result.value中保存主键
};

request.onerror = function (event) {
// error
};

要根据给定的索引键取得主键,可以使用getKey()方法。这个方法也会创建-个新的请求。但event.result.value等于主键的值,而不是包含整个对象。

1
2
3
4
5
6
7
8
var store = db.transaction('users').objectStore('users'),
index = store.index('username'),
request = index.getKey('007');

request.onsuccess = function (event) {
// success
// event.result.key中保存索引键,而event.result.value中保存主键
};

在这个例子的onsuccess事件处理程序中,event.result.value中保存的是用户ID。
任何时候,通过IDBIndex对象的下列属性都可以取得有关索引的相关信息。

  • name:索引的名字
  • keyPath:传入createIndex()中的属性路径
  • objectStore:索引的对象存储空间
  • unique:表示索引键是否唯一的布尔值

另外,通过对象存储对象的indexName属性可以访问到为该空间简历的所有索引。通过以下代码就可以知道根据存储的对象简历了哪些索引。

1
2
3
4
5
6
7
8
9
10
var store = db.transaction('users').objectStore('users'),
indexNames = store.indexName,
index,
i = 0,
len = indexNames.length;

while (i < len) {
index = store.index(indexNames[i++]);
console.log(index.name, index.keyPath, index.unique);
}

以上代码遍历了每个索引,在控制台中输出了它们的信息。
在对象存储空间上调用deleteIndex()方法并传入索引的名字可以删除索引。

1
2
var store = db.transaction('users').objectStore('users');
store.deleteIndex('username');

因为删除索引不会影响对象存储空间中的数据,所以这个操作没有任何回调函数。

8.并发问题

虽然网页中的lndexedDB提供的是异步API,但仍然存在并发操作的问题。如果浏览器的两个不同的标签页打开了同一个页面,那么一个页面试图更新另一个页面尚未准备就绪的数据库的问题就有可能发生。把数据库设置为新版本有可能导致这个问题。因此,只有当浏览器中仅有一个标签页使用数据库的情况下,调用setVersion()才能完成操作。
刚打开数据库时,要记着指定onversionchange事件处理程序。当同一个来源的另一个标签页调用setVersion()时,就会执行这个回调函数。处理这个事件的段佳方式是立即关闭数据库,从而保证版本更新顺利完成。例如:

1
2
3
4
5
6
7
8
9
10
11
var request,
database;

request = indexedDB.open('admin');
request.onsuccess = function(event) {
database = event.target.result;

database.onversionchange = function () {
database.close();
};
};

每次成功打开数据库,都应该指定onversionchange事件处理程序。
调用setVersion()时,指定请求的onblocked事件处理程序也很重要。在你想要更新数据库的版本但另一个标签页已经打开数据库的情况下,就会触发这个事件处理程序。此时,最好先通知用户关闭其他标簇页,然后再重新调用setVersion()。例如:

1
2
3
4
5
6
7
8
var request = database.setVersion('2.0');
request.onblocked = function () {
alert('Please colse all other tabs and try again.');
};

request.onsuccess = function () {
// success
};

请记住,其他标签页中的onversionchange事件处理程序也会执行。
通过指定这些事件处理程序,就能确保你的Web应用妥善地处理好IndexedDB的并发问题。

9.限制

对lndexedDB的限制很多都与对WebStorage的类似。首先,lndexedDB数据库只能由同源(相同协议、域名和端口)页面操作,因此不能跨域共享信息。换句话说,WWW.wrox.com与p2p.wrox.com 的数据库是完全独立的。
其次,每个来源的数据库占用的磁盘空间也有限制。Firefox4+目前的上限是每个源50MB,而Chrome的限制是5MB。移动设备上的Firefox最多允许保存5MB,如果超过了这个配额,将会请求用户的许可。
Firefox还有另外一个限制,即不允许本地文件访问IndexedDB。Chrome没有这个限制。如果你在本地运行本书的示例,请使用Chrome。

温习:

  • navigator.onLine及online、offline事件
  • manifest离线应用,及update()、swapCache()
  • Cookie及子Cookie的增删查,限制
  • sessionStorage、globalStorage(*)、localStorage;
  • IndexedDB操作

(完)